Merge "Change TetheringManager#RequestDispatcher as static" into main
diff --git a/Cronet/OWNERS b/Cronet/OWNERS
deleted file mode 100644
index c24680e..0000000
--- a/Cronet/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-set noparent
-file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6d8ed4a..078ccde 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,12 +1,13 @@
jchalard@google.com
junyulai@google.com
lorenzo@google.com
-martinwu@google.com
maze@google.com
motomuman@google.com
paulhu@google.com
prohr@google.com
reminv@google.com
-satk@google.com
xiaom@google.com
yuyanghuang@google.com
+
+martinwu@google.com #{LAST_RESORT_SUGGESTION}
+satk@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index b24e3ac..9e4e4a1 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -1,11 +1,12 @@
lorenzo@google.com
satk@google.com #{LAST_RESORT_SUGGESTION}
-# For cherry-picks of CLs that are already merged in aosp/master, or flaky test fixes.
+# For cherry-picks of CLs that are already merged in aosp/master, flaky test
+# fixes, or no-op refactors.
jchalard@google.com #{LAST_RESORT_SUGGESTION}
-# In addition to cherry-picks and flaky test fixes, also for APF firmware tests
-# (to verify correct behaviour of the wifi APF interpreter)
+# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
+# APF firmware tests (to verify correct behaviour of the wifi APF interpreter)
maze@google.com #{LAST_RESORT_SUGGESTION}
-# In addition to cherry-picks and flaky test fixes, also for incremental changes on NsdManager tests
-# to increase coverage for existing behavior, and testing of bug fixes in NsdManager
+# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
+# NsdManager tests
reminv@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 83619d6..39009cb 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,6 +1,15 @@
+[Builtin Hooks]
+bpfmt = true
+clang_format = true
+ktfmt = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
+ktfmt = --kotlinlang-style
+
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4cf93a8..94adc5b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,7 +1,7 @@
{
"captiveportal-networkstack-resolve-tethering-mainline-presubmit": [
{
- "name": "CtsNetTestCasesLatestSdk",
+ "name": "CtsNetTestCases",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -89,7 +89,7 @@
// Test with APK modules only, in cases where APEX is not supported, or the other modules
// were simply not updated
{
- "name": "CtsNetTestCasesLatestSdk",
+ "name": "CtsNetTestCases",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -115,7 +115,7 @@
// really exist in the field, but there is no strong guarantee, and it is required by MTS
// testing for module qualification, where modules are tested independently.
{
- "name": "CtsNetTestCasesLatestSdk",
+ "name": "CtsNetTestCases",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -158,8 +158,6 @@
},
// Run in addition to mainline-presubmit as mainline-presubmit is not
// supported in every branch.
- // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise
- // some latest APIs. Run CtsNetTestCases to get coverage of newer APIs.
{
"name": "CtsNetTestCases",
"options": [
@@ -171,18 +169,6 @@
}
]
},
- // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
- {
- "name": "CtsNetTestCasesLatestSdk",
- "options": [
- {
- "exclude-annotation": "com.android.testutils.SkipPresubmit"
- },
- {
- "exclude-annotation": "androidx.test.filters.RequiresDevice"
- }
- ]
- },
// CTS tests that target older SDKs.
{
"name": "CtsNetTestCasesMaxTargetSdk30",
@@ -267,11 +253,15 @@
},
{
"name": "FrameworksNetTests"
+ },
+ // TODO: Move to presumit after meet SLO requirement.
+ {
+ "name": "NetworkStaticLibHostPythonTests"
}
],
"mainline-presubmit": [
{
- "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -329,7 +319,7 @@
// Test with APK modules only, in cases where APEX is not supported, or the other modules
// were simply not updated
{
- "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk]",
+ "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk]",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -353,7 +343,7 @@
// really exist in the field, but there is no strong guarantee, and it is required by MTS
// testing for module qualification, where modules are tested independently.
{
- "name": "CtsNetTestCasesLatestSdk[com.google.android.tethering.apex]",
+ "name": "CtsNetTestCases[com.google.android.tethering.apex]",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -404,7 +394,7 @@
"mainline-postsubmit": [
// Tests on physical devices with SIM cards: postsubmit only for capacity constraints
{
- "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"keywords": ["sim"]
},
{
@@ -418,7 +408,16 @@
},
// Postsubmit on virtual devices to monitor flakiness of @SkipMainlinePresubmit methods
{
- "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ // TODO: upgrade to presubmit. Postsubmit on virtual devices to monitor flakiness only.
+ {
+ "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"options": [
{
"exclude-annotation": "androidx.test.filters.RequiresDevice"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index e84573b..5cf5528 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -32,16 +32,19 @@
java_defaults {
name: "TetheringExternalLibs",
+ defaults: [
+ "TetheringApiLevel",
+ ],
// Libraries not including Tethering's own framework-tethering (different flavors of that one
// are needed depending on the build rule)
libs: [
"connectivity-internal-api-util",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-connectivity-t.stubs.module_lib",
"framework-statsd.stubs.module_lib",
- "framework-wifi",
- "framework-bluetooth",
+ "framework-wifi.stubs.module_lib",
+ "framework-bluetooth.stubs.module_lib",
"unsupportedappusage",
],
defaults_visibility: ["//visibility:private"],
@@ -54,6 +57,7 @@
"src/**/*.java",
":framework-connectivity-shared-srcs",
":services-tethering-shared-srcs",
+ ":statslog-connectivity-java-gen",
":statslog-tethering-java-gen",
],
static_libs: [
@@ -69,18 +73,13 @@
"android.hardware.tetheroffload.control-V1.0-java",
"android.hardware.tetheroffload.control-V1.1-java",
"android.hidl.manager-V1.2-java",
- "net-utils-framework-common",
- "net-utils-device-common",
- "net-utils-device-common-bpf",
- "net-utils-device-common-ip",
- "net-utils-device-common-netlink",
- "net-utils-device-common-struct",
- "net-utils-device-common-struct-base",
+ "net-utils-connectivity-apks",
"netd-client",
"tetheringstatsprotos",
],
defaults: ["TetheringExternalLibs"],
libs: [
+ "framework-annotations-lib",
"framework-tethering.impl",
],
manifest: "AndroidManifestBase.xml",
@@ -95,7 +94,6 @@
defaults: [
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
- "TetheringApiLevel",
"TetheringReleaseTargetSdk",
],
static_libs: [
@@ -111,7 +109,6 @@
name: "TetheringApiStableLib",
defaults: [
"TetheringAndroidLibraryDefaults",
- "TetheringApiLevel",
"TetheringReleaseTargetSdk",
],
static_libs: [
@@ -200,7 +197,6 @@
name: "Tethering",
defaults: [
"TetheringAppDefaults",
- "TetheringApiLevel",
],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
@@ -208,13 +204,13 @@
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
+ updatable: true,
}
android_app {
name: "TetheringNext",
defaults: [
"TetheringAppDefaults",
- "TetheringApiLevel",
"ConnectivityNextEnableDefaults",
],
static_libs: ["TetheringApiCurrentLib"],
@@ -226,6 +222,7 @@
lint: {
error_checks: ["NewApi"],
},
+ updatable: true,
}
sdk {
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8a5d249..0c05354 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -98,10 +98,8 @@
],
canned_fs_config: "canned_fs_config",
bpfs: [
- "block.o",
"clatd.o",
"dscpPolicy.o",
- "gentle.o",
"netd.o",
"offload.o",
"offload@mainline.o",
@@ -115,7 +113,7 @@
"current_sdkinfo",
"netbpfload.33rc",
"netbpfload.35rc",
- "ot-daemon.init.34rc",
+ "ot-daemon.34rc",
],
manifest: "manifest.json",
key: "com.android.tethering.key",
@@ -216,6 +214,7 @@
"android.net.nsd",
"android.net.thread",
"android.net.wear",
+ "android.net.http.internal",
],
},
}
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 0df9047..af061e4 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -198,4 +198,13 @@
public String toString() {
return "Netd used";
}
+
+ @Override
+ public int getLastMaxConnectionAndResetToCurrent() {
+ return 0;
+ }
+
+ @Override
+ public void clearConnectionCounters() {
+ }
}
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 4d1e7ef..b460f0d 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -19,6 +19,7 @@
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import android.system.ErrnoException;
import android.system.Os;
@@ -108,6 +109,22 @@
// TODO: Add IPv6 rule count.
private final SparseArray<Integer> mRule4CountOnUpstream = new SparseArray<>();
+ private final boolean mSupportActiveSessionsMetrics;
+ /**
+ * Tracks the current number of tethering connections and the maximum
+ * observed since the last metrics collection. Used to provide insights
+ * into the distribution of active tethering sessions for metrics reporting.
+
+ * These variables are accessed on the handler thread, which includes:
+ * 1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
+ * 2. ConntrackEvents indicating the removal of a tethering client,
+ * triggering the removal of associated rules.
+ * 3. Removal of the last IpServer, which resets counters to handle
+ * potential synchronization issues.
+ */
+ private int mLastMaxConnectionCount = 0;
+ private int mCurrentConnectionCount = 0;
+
public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
mLog = deps.getSharedLog().forSubComponent(TAG);
@@ -156,6 +173,9 @@
} catch (ErrnoException e) {
mLog.e("Could not clear mBpfDevMap: " + e);
}
+
+ mSupportActiveSessionsMetrics = deps.isFeatureEnabled(deps.getContext(),
+ TETHER_ACTIVE_SESSIONS_METRICS);
}
@Override
@@ -350,6 +370,12 @@
final int upstreamIfindex = (int) key.iif;
int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */);
mRule4CountOnUpstream.put(upstreamIfindex, ++count);
+
+ if (mSupportActiveSessionsMetrics) {
+ mCurrentConnectionCount++;
+ mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
+ mLastMaxConnectionCount);
+ }
} else {
mBpfUpstream4Map.insertEntry(key, value);
}
@@ -359,6 +385,7 @@
} catch (IllegalStateException e) {
// Silent if the rule already exists. Note that the errno EEXIST was rethrown as
// IllegalStateException. See BpfMap#insertEntry.
+ return false;
}
return true;
}
@@ -384,6 +411,10 @@
} else {
mRule4CountOnUpstream.put(upstreamIfindex, count);
}
+
+ if (mSupportActiveSessionsMetrics) {
+ mCurrentConnectionCount--;
+ }
} else {
if (!mBpfUpstream4Map.deleteEntry(key)) return false; // Rule did not exist
}
@@ -464,14 +495,16 @@
@Override
public String toString() {
- return String.join(", ", new String[] {
- mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
- mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
- mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
- mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
- mapStatus(mBpfStatsMap, "mBpfStatsMap"),
- mapStatus(mBpfLimitMap, "mBpfLimitMap"),
- mapStatus(mBpfDevMap, "mBpfDevMap")
+ return String.join(", ", new String[]{
+ mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
+ mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
+ mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
+ mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
+ mapStatus(mBpfStatsMap, "mBpfStatsMap"),
+ mapStatus(mBpfLimitMap, "mBpfLimitMap"),
+ mapStatus(mBpfDevMap, "mBpfDevMap"),
+ "mCurrentConnectionCount=" + mCurrentConnectionCount,
+ "mLastMaxConnectionCount=" + mLastMaxConnectionCount
});
}
@@ -506,4 +539,17 @@
return 0;
}
+
+ /** Get last max connection count and reset to current count. */
+ public int getLastMaxConnectionAndResetToCurrent() {
+ final int ret = mLastMaxConnectionCount;
+ mLastMaxConnectionCount = mCurrentConnectionCount;
+ return ret;
+ }
+
+ /** Clear current connection count. */
+ public void clearConnectionCounters() {
+ mCurrentConnectionCount = 0;
+ mLastMaxConnectionCount = 0;
+ }
}
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index d28a397..cb8bcc9 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -140,6 +140,8 @@
/**
* Adds a tethering IPv4 offload rule to appropriate BPF map.
+ *
+ * @return true iff the map was modified, false if the key already exists or there was an error.
*/
public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
@NonNull Tether4Value value);
@@ -200,5 +202,11 @@
* Remove interface index mapping.
*/
public abstract boolean removeDevMap(int ifIndex);
+
+ /** Get last max connection count and reset to current count. */
+ public abstract int getLastMaxConnectionAndResetToCurrent();
+
+ /** Clear current connection count. */
+ public abstract void clearConnectionCounters();
}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 9fa073b..2f3307a 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -29,6 +29,7 @@
"//packages/modules/Connectivity/framework-t",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/staticlibs",
// Using for test only
"//cts/tests/netlegacy22.api",
@@ -46,10 +47,14 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
+ "//packages/modules/NetworkStack",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
- stub_only_libs: ["framework-connectivity.stubs.module_lib"],
+ stub_only_libs: [
+ "framework-connectivity.stubs.module_lib",
+ "sdk_module-lib_current_framework-wifi",
+ ],
jarjar_rules: ":framework-tethering-jarjar-rules",
installable: true,
@@ -58,6 +63,7 @@
permitted_packages: ["android.net"],
lint: {
strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
},
aconfig_declarations: [
"com.android.net.flags-aconfig",
@@ -96,10 +102,17 @@
srcs: [
":framework-tethering-srcs",
],
- libs: ["framework-connectivity.stubs.module_lib"],
+ libs: [
+ "framework-connectivity.stubs.module_lib",
+ "sdk_module-lib_current_framework-wifi",
+ ],
+ static_libs: [
+ "com.android.net.flags-aconfig-java",
+ ],
aidl: {
include_dirs: [
"packages/modules/Connectivity/framework/aidl-export",
+ "packages/modules/Wifi/framework/aidl-export",
],
},
apex_available: ["com.android.tethering"],
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index a287b42..3efaac2 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -47,6 +47,7 @@
field public static final int TETHERING_INVALID = -1; // 0xffffffff
field public static final int TETHERING_NCM = 4; // 0x4
field public static final int TETHERING_USB = 1; // 0x1
+ field @FlaggedApi("com.android.net.flags.tethering_request_virtual") public static final int TETHERING_VIRTUAL = 7; // 0x7
field public static final int TETHERING_WIFI = 0; // 0x0
field public static final int TETHERING_WIFI_P2P = 3; // 0x3
field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
@@ -101,6 +102,7 @@
method public int getConnectivityScope();
method @Nullable public android.net.LinkAddress getLocalIpv4Address();
method public boolean getShouldShowEntitlementUi();
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
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);
@@ -113,6 +115,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
}
diff --git a/Tethering/common/TetheringLib/lint-baseline.xml b/Tethering/common/TetheringLib/lint-baseline.xml
new file mode 100644
index 0000000..5171efb
--- /dev/null
+++ b/Tethering/common/TetheringLib/lint-baseline.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+ <issue
+ id="FlaggedApi"
+ message="Field `TETHERING_VIRTUAL` is a flagged API and should be inside an `if (Flags.tetheringRequestVirtual())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL) to transfer requirement to caller`)"
+ errorLine1=" public static final int MAX_TETHERING_TYPE = TETHERING_VIRTUAL;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/Tethering/common/TetheringLib/src/android/net/TetheringManager.java"
+ line="211"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `TetheringRequest()` is a flagged API and should be inside an `if (Flags.tetheringRequestWithSoftApConfig())` check (or annotate the surrounding method `build` with `@FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG) to transfer requirement to caller`)"
+ errorLine1=" return new TetheringRequest(mBuilderParcel);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/Tethering/common/TetheringLib/src/android/net/TetheringManager.java"
+ line="814"
+ column="24"/>
+ </issue>
+
+</issues>
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index b148916..411971d 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -26,6 +26,8 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Context;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
@@ -38,6 +40,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.net.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -62,14 +65,6 @@
*/
@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;
@@ -195,10 +190,22 @@
public static final int TETHERING_WIGIG = 6;
/**
+ * VIRTUAL tethering type.
+ *
+ * This tethering type is for providing external network to virtual machines
+ * running on top of Android devices, which are created and managed by
+ * AVF(Android Virtualization Framework).
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_VIRTUAL)
+ @SystemApi
+ public static final int TETHERING_VIRTUAL = 7;
+
+ /**
* The int value of last tethering type.
* @hide
*/
- public static final int MAX_TETHERING_TYPE = TETHERING_WIGIG;
+ public static final int MAX_TETHERING_TYPE = TETHERING_VIRTUAL;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -688,7 +695,11 @@
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
- private TetheringRequest(@NonNull final TetheringRequestParcel request) {
+ /**
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ public TetheringRequest(@NonNull final TetheringRequestParcel request) {
mRequestParcel = request;
}
@@ -696,7 +707,7 @@
mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
}
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
@NonNull
public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
@Override
@@ -710,13 +721,13 @@
}
};
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
@Override
public int describeContents() {
return 0;
}
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(mRequestParcel, flags);
@@ -735,6 +746,7 @@
mBuilderParcel.exemptFromEntitlementCheck = false;
mBuilderParcel.showProvisioningUi = true;
mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
+ mBuilderParcel.softApConfig = null;
}
/**
@@ -794,6 +806,30 @@
return this;
}
+ /**
+ * Set the desired SoftApConfiguration for {@link #TETHERING_WIFI}. If this is null or
+ * not set, then the persistent tethering SoftApConfiguration from
+ * {@link WifiManager#getSoftApConfiguration()} will be used.
+ * </p>
+ * If TETHERING_WIFI is already enabled and a new request is made with a different
+ * SoftApConfiguration, the request will be accepted if the device can support an
+ * additional tethering Wi-Fi AP interface. Otherwise, the request will be rejected.
+ *
+ * @param softApConfig SoftApConfiguration to use.
+ * @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setSoftApConfiguration(@Nullable SoftApConfiguration softApConfig) {
+ if (mBuilderParcel.tetheringType != TETHERING_WIFI) {
+ throw new IllegalArgumentException(
+ "SoftApConfiguration can only be set for TETHERING_WIFI");
+ }
+ mBuilderParcel.softApConfig = softApConfig;
+ return this;
+ }
+
/** Build {@link TetheringRequest} with the currently set configuration. */
@NonNull
public TetheringRequest build() {
@@ -875,6 +911,15 @@
}
/**
+ * Get the desired SoftApConfiguration of the request, if one was specified.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @Nullable
+ public SoftApConfiguration getSoftApConfiguration() {
+ return mRequestParcel.softApConfig;
+ }
+
+ /**
* Get a TetheringRequestParcel from the configuration
* @hide
*/
@@ -887,9 +932,33 @@
return "TetheringRequest [ type= " + mRequestParcel.tetheringType
+ ", localIPv4Address= " + mRequestParcel.localIPv4Address
+ ", staticClientAddress= " + mRequestParcel.staticClientAddress
- + ", exemptFromEntitlementCheck= "
- + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= "
- + mRequestParcel.showProvisioningUi + " ]";
+ + ", exemptFromEntitlementCheck= " + mRequestParcel.exemptFromEntitlementCheck
+ + ", showProvisioningUi= " + mRequestParcel.showProvisioningUi
+ + ", softApConfig= " + mRequestParcel.softApConfig
+ + " ]";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof TetheringRequest otherRequest)) return false;
+ TetheringRequestParcel parcel = getParcel();
+ TetheringRequestParcel otherParcel = otherRequest.getParcel();
+ return parcel.tetheringType == otherParcel.tetheringType
+ && Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
+ && Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
+ && parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
+ && parcel.showProvisioningUi == otherParcel.showProvisioningUi
+ && parcel.connectivityScope == otherParcel.connectivityScope
+ && Objects.equals(parcel.softApConfig, otherParcel.softApConfig);
+ }
+
+ @Override
+ public int hashCode() {
+ TetheringRequestParcel parcel = getParcel();
+ return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
+ parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
+ parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig);
}
}
@@ -1343,6 +1412,9 @@
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
public void registerTetheringEventCallback(@NonNull Executor executor,
@NonNull TetheringEventCallback callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "registerTetheringEventCallback caller:" + callerPkg);
@@ -1497,6 +1569,8 @@
Manifest.permission.ACCESS_NETWORK_STATE
})
public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
+ Objects.requireNonNull(callback);
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "unregisterTetheringEventCallback caller:" + callerPkg);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index f13c970..ea7a353 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -17,6 +17,7 @@
package android.net;
import android.net.LinkAddress;
+import android.net.wifi.SoftApConfiguration;
/**
* Configuration details for requesting tethering.
@@ -29,4 +30,5 @@
boolean exemptFromEntitlementCheck;
boolean showProvisioningUi;
int connectivityScope;
+ SoftApConfiguration softApConfig;
}
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 9e0c970..b4c1b8a 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -34,7 +34,6 @@
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;
@@ -45,10 +44,9 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
-import android.net.RoutingCoordinatorManager;
import android.net.TetheredClient;
import android.net.TetheringManager;
-import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpLeaseParcelable;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
@@ -73,21 +71,17 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetdUtils;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
import com.android.net.module.util.ip.InterfaceController;
-import com.android.net.module.util.ip.IpNeighborMonitor;
-import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
import com.android.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.networkstack.tethering.util.StateMachineShim;
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -189,12 +183,6 @@
return new DadProxy(handler, ifParams);
}
- /** Create an IpNeighborMonitor to be used by this IpServer */
- public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log,
- IpNeighborMonitor.NeighborEventConsumer consumer) {
- return new IpNeighborMonitor(handler, log, consumer);
- }
-
/** Create a RouterAdvertisementDaemon instance to be used by IpServer.*/
public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) {
return new RouterAdvertisementDaemon(ifParams);
@@ -234,13 +222,11 @@
public static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IPSERVER + 9;
// new IPv6 tethering parameters need to be processed
public static final int CMD_IPV6_TETHER_UPDATE = BASE_IPSERVER + 10;
- // new neighbor cache entry on our interface
- public static final int CMD_NEIGHBOR_EVENT = BASE_IPSERVER + 11;
// request from DHCP server that it wants to have a new prefix
- public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 12;
+ public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 11;
// request from PrivateAddressCoordinator to restart tethering.
- public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 13;
- public static final int CMD_SERVICE_FAILED_TO_START = BASE_IPSERVER + 14;
+ public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 12;
+ public static final int CMD_SERVICE_FAILED_TO_START = BASE_IPSERVER + 13;
private final State mInitialState;
private final State mLocalHotspotState;
@@ -252,11 +238,8 @@
private final INetd mNetd;
@NonNull
private final BpfCoordinator mBpfCoordinator;
- // Contains null if the connectivity module is unsupported, as the routing coordinator is not
- // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
- // must be able to find all classes at runtime.
@NonNull
- private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+ private final RoutingCoordinatorManager mRoutingCoordinator;
private final Callback mCallback;
private final InterfaceController mInterfaceCtrl;
private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -301,18 +284,9 @@
private List<TetheredClient> mDhcpLeases = Collections.emptyList();
private int mLastIPv6UpstreamIfindex = 0;
- private boolean mUpstreamSupportsBpf = false;
@NonNull
private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
- private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
- public void accept(NeighborEvent e) {
- sendMessage(CMD_NEIGHBOR_EVENT, e);
- }
- }
-
- private final IpNeighborMonitor mIpNeighborMonitor;
-
private LinkAddress mIpv4Address;
private final TetheringMetrics mTetheringMetrics;
@@ -323,7 +297,7 @@
public IpServer(
String ifaceName, Handler handler, int interfaceType, SharedLog log,
INetd netd, @NonNull BpfCoordinator bpfCoordinator,
- @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
+ RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
@@ -331,7 +305,7 @@
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
mBpfCoordinator = bpfCoordinator;
- mRoutingCoordinator = routingCoordinator;
+ mRoutingCoordinator = routingCoordinatorManager;
mCallback = callback;
mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
mIfaceName = ifaceName;
@@ -346,15 +320,6 @@
mLastError = TETHER_ERROR_NO_ERROR;
mServingMode = STATE_AVAILABLE;
- mIpNeighborMonitor = mDeps.getIpNeighborMonitor(getHandler(), mLog,
- new MyNeighborEventConsumer());
-
- // IP neighbor monitor monitors the neighbor events for adding/removing IPv6 downstream rule
- // per client. If BPF offload is not supported, don't start listening for neighbor events.
- if (mBpfCoordinator.isUsingBpfOffload() && !mIpNeighborMonitor.start()) {
- mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName);
- }
-
mInitialState = new InitialState();
mLocalHotspotState = new LocalHotspotState();
mTetheredState = new TetheredState();
@@ -410,6 +375,22 @@
return mIpv4Address;
}
+ /** The IPv6 upstream interface index */
+ public int getIpv6UpstreamIfindex() {
+ return mLastIPv6UpstreamIfindex;
+ }
+
+ /** The IPv6 upstream interface prefixes */
+ @NonNull
+ public Set<IpPrefix> getIpv6UpstreamPrefixes() {
+ return Collections.unmodifiableSet(mLastIPv6UpstreamPrefixes);
+ }
+
+ /** The interface parameters which IpServer is using */
+ public InterfaceParams getInterfaceParams() {
+ return mInterfaceParams;
+ }
+
/**
* Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
* thread.
@@ -419,7 +400,7 @@
}
/** Enable this IpServer. IpServer state machine will be tethered or localHotspot state. */
- public void enable(final int requestedState, final TetheringRequestParcel request) {
+ public void enable(final int requestedState, final TetheringRequest request) {
sendMessage(CMD_TETHER_REQUESTED, requestedState, 0, request);
}
@@ -813,14 +794,15 @@
setRaParams(params);
// Not support BPF on virtual upstream interface
- final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
- updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamPrefixes,
- upstreamIfIndex, upstreamPrefixes, upstreamSupportsBpf);
+ // mBpfCoordinator#updateIpv6UpstreamInterface must be called before updating
+ // mLastIPv6UpstreamIfindex and mLastIPv6UpstreamPrefixes because BpfCoordinator will call
+ // IpServer#getIpv6UpstreamIfindex and IpServer#getIpv6UpstreamPrefixes to retrieve current
+ // upstream interface index and prefixes when handling upstream changes.
+ mBpfCoordinator.updateIpv6UpstreamInterface(this, upstreamIfIndex, upstreamPrefixes);
mLastIPv6LinkProperties = v6only;
mLastIPv6UpstreamIfindex = upstreamIfIndex;
mLastIPv6UpstreamPrefixes = upstreamPrefixes;
- mUpstreamSupportsBpf = upstreamSupportsBpf;
if (mDadProxy != null) {
mDadProxy.setUpstreamIface(upstreamIfaceParams);
}
@@ -839,47 +821,25 @@
private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
try {
- if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
- // TODO : remove this call in favor of using the LocalNetworkConfiguration
- // correctly, which will let ConnectivityService do it automatically.
- mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
- } else {
- mNetd.networkAddInterface(netId, ifaceName);
- }
- } catch (ServiceSpecificException | RemoteException e) {
+ // TODO : remove this call in favor of using the LocalNetworkConfiguration
+ // correctly, which will let ConnectivityService do it automatically.
+ mRoutingCoordinator.addInterfaceToNetwork(netId, ifaceName);
+ } catch (ServiceSpecificException e) {
mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
}
}
- private void addInterfaceForward(@NonNull final String fromIface,
- @NonNull final String toIface) throws ServiceSpecificException, RemoteException {
- if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
- mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
- } else {
- mNetd.tetherAddForward(fromIface, toIface);
- mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
- }
+ private void addInterfaceForward(@NonNull final String fromIface, @NonNull final String toIface)
+ throws ServiceSpecificException {
+ mRoutingCoordinator.addInterfaceForward(fromIface, toIface);
}
private void removeInterfaceForward(@NonNull final String fromIface,
@NonNull final String toIface) {
- if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
- try {
- mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
- } catch (ServiceSpecificException e) {
- mLog.e("Exception in removeInterfaceForward", e);
- }
- } else {
- try {
- mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Exception in ipfwdRemoveInterfaceForward", e);
- }
- try {
- mNetd.tetherRemoveForward(fromIface, toIface);
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Exception in disableNat", e);
- }
+ try {
+ mRoutingCoordinator.removeInterfaceForward(fromIface, toIface);
+ } catch (RuntimeException e) {
+ mLog.e("Exception in removeInterfaceForward", e);
}
}
@@ -964,77 +924,6 @@
}
}
- private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
- return supportsBpf ? ifindex : NO_UPSTREAM;
- }
-
- // Handles updates to IPv6 forwarding rules if the upstream or its prefixes change.
- private void updateIpv6ForwardingRules(int prevUpstreamIfindex,
- @NonNull Set<IpPrefix> prevUpstreamPrefixes, int upstreamIfindex,
- @NonNull Set<IpPrefix> upstreamPrefixes, boolean upstreamSupportsBpf) {
- // If the upstream interface has changed, remove all rules and re-add them with the new
- // upstream interface. If upstream is a virtual network, treated as no upstream.
- if (prevUpstreamIfindex != upstreamIfindex
- || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
- mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
- getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
- upstreamPrefixes);
- }
- }
-
- // Handles updates to IPv6 downstream rules if a neighbor event is received.
- private void addOrRemoveIpv6Downstream(NeighborEvent e) {
- // mInterfaceParams must be non-null or the event would not have arrived.
- if (e == null) return;
- if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
- || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
- return;
- }
-
- // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
- // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
- // never add rules with a null MAC, only delete them.
- MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
- Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
- getInterfaceIndexForRule(mLastIPv6UpstreamIfindex, mUpstreamSupportsBpf),
- mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
- if (e.isValid()) {
- mBpfCoordinator.addIpv6DownstreamRule(this, rule);
- } else {
- mBpfCoordinator.removeIpv6DownstreamRule(this, rule);
- }
- }
-
- // TODO: consider moving into BpfCoordinator.
- private void updateClientInfoIpv4(NeighborEvent e) {
- if (e == null) return;
- if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
- || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
- return;
- }
-
- // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
- // ignored. Do this here instead of in the ClientInfo constructor to ensure that
- // IpServer never add clients with a null MAC, only delete them.
- final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
- final ClientInfo clientInfo = new ClientInfo(mInterfaceParams.index,
- mInterfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
- if (e.isValid()) {
- mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo);
- } else {
- mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
- }
- }
-
- private void handleNeighborEvent(NeighborEvent e) {
- if (mInterfaceParams != null
- && mInterfaceParams.index == e.ifindex
- && mInterfaceParams.hasMacAddress) {
- addOrRemoveIpv6Downstream(e);
- updateClientInfoIpv4(e);
- }
- }
-
private byte getHopLimit(String upstreamIface, int adjustTTL) {
try {
int upstreamHopLimit = Integer.parseUnsignedInt(
@@ -1069,7 +958,6 @@
switch (what) {
// Suppress some CMD_* to avoid log flooding.
case CMD_IPV6_TETHER_UPDATE:
- case CMD_NEIGHBOR_EVENT:
break;
default:
mLog.log(state.getName() + " got "
@@ -1092,18 +980,18 @@
mLinkProperties.setInterfaceName(mIfaceName);
}
- private void maybeConfigureStaticIp(final TetheringRequestParcel request) {
+ private void maybeConfigureStaticIp(final TetheringRequest request) {
// Ignore static address configuration if they are invalid or null. In theory, static
// addresses should not be invalid here because TetheringManager do not allow caller to
// specify invalid static address configuration.
- if (request == null || request.localIPv4Address == null
- || request.staticClientAddress == null || !checkStaticAddressConfiguration(
- request.localIPv4Address, request.staticClientAddress)) {
+ if (request == null || request.getLocalIpv4Address() == null
+ || request.getClientStaticIpv4Address() == null || !checkStaticAddressConfiguration(
+ request.getLocalIpv4Address(), request.getClientStaticIpv4Address())) {
return;
}
- mStaticIpv4ServerAddr = request.localIPv4Address;
- mStaticIpv4ClientAddr = request.staticClientAddress;
+ mStaticIpv4ServerAddr = request.getLocalIpv4Address();
+ mStaticIpv4ClientAddr = request.getClientStaticIpv4Address();
}
class InitialState extends State {
@@ -1120,11 +1008,11 @@
mLastError = TETHER_ERROR_NO_ERROR;
switch (message.arg1) {
case STATE_LOCAL_ONLY:
- maybeConfigureStaticIp((TetheringRequestParcel) message.obj);
+ maybeConfigureStaticIp((TetheringRequest) message.obj);
transitionTo(mLocalHotspotState);
break;
case STATE_TETHERED:
- maybeConfigureStaticIp((TetheringRequestParcel) message.obj);
+ maybeConfigureStaticIp((TetheringRequest) message.obj);
transitionTo(mTetheredState);
break;
default:
@@ -1141,14 +1029,6 @@
}
}
- private void startConntrackMonitoring() {
- mBpfCoordinator.startMonitoring(this);
- }
-
- private void stopConntrackMonitoring() {
- mBpfCoordinator.stopMonitoring(this);
- }
-
abstract class BaseServingState extends State {
private final int mDesiredInterfaceState;
@@ -1158,7 +1038,7 @@
@Override
public void enter() {
- startConntrackMonitoring();
+ mBpfCoordinator.addIpServer(IpServer.this);
startServingInterface();
@@ -1226,7 +1106,7 @@
}
stopIPv4();
- stopConntrackMonitoring();
+ mBpfCoordinator.removeIpServer(IpServer.this);
resetLinkProperties();
@@ -1268,6 +1148,7 @@
case CMD_SERVICE_FAILED_TO_START:
mLog.e("start serving fail, error: " + message.arg1);
transitionTo(mInitialState);
+ break;
default:
return false;
}
@@ -1397,8 +1278,8 @@
for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
mUpstreamIfaceSet = null;
- mBpfCoordinator.updateAllIpv6Rules(
- IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM, Set.of());
+ mBpfCoordinator.updateIpv6UpstreamInterface(IpServer.this, NO_UPSTREAM,
+ Collections.emptySet());
}
private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1464,7 +1345,7 @@
mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
try {
addInterfaceForward(mIfaceName, ifname);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RuntimeException e) {
mLog.e("Exception enabling iface forward", e);
cleanupUpstream();
mLastError = TETHER_ERROR_ENABLE_FORWARDING_ERROR;
@@ -1473,9 +1354,6 @@
}
}
break;
- case CMD_NEIGHBOR_EVENT:
- handleNeighborEvent((NeighborEvent) message.obj);
- break;
default:
return false;
}
@@ -1515,9 +1393,6 @@
class UnavailableState extends State {
@Override
public void enter() {
- // TODO: move mIpNeighborMonitor.stop() to TetheredState#exit, and trigger a neighbours
- // dump after starting mIpNeighborMonitor.
- mIpNeighborMonitor.stop();
mLastError = TETHER_ERROR_NO_ERROR;
sendInterfaceState(STATE_UNAVAILABLE);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 00d9152..75ab9ec 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -27,16 +27,19 @@
import static android.system.OsConstants.ETH_P_IP;
import static android.system.OsConstants.ETH_P_IPV6;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.TetheringUtils.getTetheringJniLibraryName;
import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.INetd;
import android.net.IpPrefix;
import android.net.LinkProperties;
@@ -65,6 +68,7 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
@@ -76,11 +80,15 @@
import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.ip.ConntrackMonitor;
import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer;
+import com.android.net.module.util.ip.IpNeighborMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
import com.android.net.module.util.netlink.ConntrackMessage;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
+import com.android.server.ConnectivityStatsLog;
import java.io.IOException;
import java.net.Inet4Address;
@@ -145,6 +153,13 @@
@VisibleForTesting
static final int CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS = 60_000;
+ // The interval is set to 5 minutes to strike a balance between minimizing
+ // the amount of metrics data uploaded and providing sufficient resolution
+ // to track changes in forwarding rules. This choice considers the minimum
+ // push metrics sampling interval of 5 minutes and the 3-minute timeout
+ // for forwarding rules.
+ @VisibleForTesting
+ static final int CONNTRACK_METRICS_UPDATE_INTERVAL_MS = 300_000;
@VisibleForTesting
static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432_000;
@VisibleForTesting
@@ -181,6 +196,10 @@
private final BpfCoordinatorShim mBpfCoordinatorShim;
@NonNull
private final BpfConntrackEventConsumer mBpfConntrackEventConsumer;
+ @NonNull
+ private final IpNeighborMonitor mIpNeighborMonitor;
+ @NonNull
+ private final BpfNeighborEventConsumer mBpfNeighborEventConsumer;
// True if BPF offload is supported, false otherwise. The BPF offload could be disabled by
// a runtime resource overlay package or device configuration. This flag is only initialized
@@ -189,14 +208,6 @@
// to make it simpler. See also TetheringConfiguration.
private final boolean mIsBpfEnabled;
- // Tracks whether BPF tethering is started or not. This is set by tethering before it
- // starts the first IpServer and is cleared by tethering shortly before the last IpServer
- // is stopped. Note that rule updates (especially deletions, but sometimes additions as
- // well) may arrive when this is false. If they do, they must be communicated to netd.
- // Changes in data limits may also arrive when this is false, and if they do, they must
- // also be communicated to netd.
- private boolean mPollingStarted = false;
-
// Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert
// quota is interface independent and global for tether offload.
private long mRemainingAlertQuota = QUOTA_UNLIMITED;
@@ -279,9 +290,6 @@
private final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>>
mTetherClients = new HashMap<>();
- // Set for which downstream is monitoring the conntrack netlink message.
- private final Set<IpServer> mMonitoringIpServers = new HashSet<>();
-
// Map of upstream interface IPv4 address to interface index.
// TODO: consider making the key to be unique because the upstream address is not unique. It
// is okay for now because there have only one upstream generally.
@@ -303,24 +311,37 @@
@Nullable
private UpstreamInfo mIpv4UpstreamInfo = null;
+ // The IpServers that are currently served by BpfCoordinator.
+ private final ArraySet<IpServer> mServedIpServers = new ArraySet<>();
+
// Runnable that used by scheduling next polling of stats.
private final Runnable mScheduledPollingStats = () -> {
updateForwardedStats();
- maybeSchedulePollingStats();
+ schedulePollingStats();
};
// Runnable that used by scheduling next refreshing of conntrack timeout.
private final Runnable mScheduledConntrackTimeoutUpdate = () -> {
refreshAllConntrackTimeouts();
- maybeScheduleConntrackTimeoutUpdate();
+ scheduleConntrackTimeoutUpdate();
+ };
+
+ private final boolean mSupportActiveSessionsMetrics;
+
+ // Runnable that used by scheduling next refreshing of conntrack metrics sampling.
+ private final Runnable mScheduledConntrackMetricsSampling = () -> {
+ uploadConntrackMetricsSample();
+ scheduleConntrackMetricsSampling();
};
// TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
- @VisibleForTesting
public abstract static class Dependencies {
/** Get handler. */
@NonNull public abstract Handler getHandler();
+ /** Get context. */
+ @NonNull public abstract Context getContext();
+
/** Get netd. */
@NonNull public abstract INetd getNetd();
@@ -338,6 +359,11 @@
return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
}
+ /** Get ip neighbor monitor */
+ @NonNull public IpNeighborMonitor getIpNeighborMonitor(NeighborEventConsumer consumer) {
+ return new IpNeighborMonitor(getHandler(), getSharedLog(), consumer);
+ }
+
/** Get interface information for a given interface. */
@NonNull public InterfaceParams getInterfaceParams(String ifName) {
return InterfaceParams.getByName(ifName);
@@ -468,6 +494,19 @@
return null;
}
}
+
+ /** Send a TetheringActiveSessionsReported event. */
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ ConnectivityStatsLog.write(ConnectivityStatsLog.TETHERING_ACTIVE_SESSIONS_REPORTED,
+ lastMaxSessionCount);
+ }
+
+ /**
+ * @see DeviceConfigUtils#isTetheringFeatureEnabled
+ */
+ public boolean isFeatureEnabled(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+ }
}
@VisibleForTesting
@@ -485,6 +524,9 @@
mBpfConntrackEventConsumer = new BpfConntrackEventConsumer();
mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer);
+ mBpfNeighborEventConsumer = new BpfNeighborEventConsumer();
+ mIpNeighborMonitor = mDeps.getIpNeighborMonitor(mBpfNeighborEventConsumer);
+
BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
try {
mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
@@ -501,52 +543,60 @@
if (!mBpfCoordinatorShim.isInitialized()) {
mLog.e("Bpf shim not initialized");
}
+
+ // BPF IPv4 forwarding only supports on S+.
+ mSupportActiveSessionsMetrics = mDeps.isAtLeastS()
+ && mDeps.isFeatureEnabled(mDeps.getContext(), TETHER_ACTIVE_SESSIONS_METRICS);
}
/**
- * Start BPF tethering offload stats polling when the first upstream is started.
+ * Start BPF tethering offload stats and conntrack polling.
* Note that this can be only called on handler thread.
- * TODO: Perhaps check BPF support before starting.
- * TODO: Start the stats polling only if there is any client on the downstream.
*/
- public void startPolling() {
- if (mPollingStarted) return;
-
- if (!isUsingBpf()) {
- mLog.i("BPF is not using");
- return;
+ private void startStatsAndConntrackPolling() {
+ schedulePollingStats();
+ scheduleConntrackTimeoutUpdate();
+ if (mSupportActiveSessionsMetrics) {
+ scheduleConntrackMetricsSampling();
}
- mPollingStarted = true;
- maybeSchedulePollingStats();
- maybeScheduleConntrackTimeoutUpdate();
-
- mLog.i("Polling started");
+ mLog.i("Polling started.");
}
/**
- * Stop BPF tethering offload stats polling.
+ * Stop BPF tethering offload stats and conntrack polling.
* The data limit cleanup and the tether stats maps cleanup are not implemented here.
* These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
* last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
* functionality.
* Note that this can be only called on handler thread.
*/
- public void stopPolling() {
- if (!mPollingStarted) return;
-
+ private void stopStatsAndConntrackPolling() {
// Stop scheduled polling conntrack timeout.
if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
}
+ // Stop scheduled polling conntrack metrics sampling and
+ // clear counters in case there is any counter unsync problem
+ // previously due to possible bpf failures.
+ // Normally this won't happen because all clients are cleared before
+ // reaching here. See IpServer.BaseServingState#exit().
+ if (mSupportActiveSessionsMetrics) {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+ // Avoid sending metrics when tethering is about to close.
+ // This leads to a missing final sample before disconnect
+ // but avoids possibly duplicating the last metric in the upload.
+ mBpfCoordinatorShim.clearConnectionCounters();
+ }
// Stop scheduled polling stats and poll the latest stats from BPF maps.
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
}
updateForwardedStats();
- mPollingStarted = false;
- mLog.i("Polling stopped");
+ mLog.i("Polling stopped.");
}
/**
@@ -567,7 +617,6 @@
/**
* Start conntrack message monitoring.
- * Note that this can be only called on handler thread.
*
* TODO: figure out a better logging for non-interesting conntrack message.
* For example, the following logging is an IPCTNL_MSG_CT_GET message but looks scary.
@@ -587,45 +636,23 @@
* +------------------+--------------------------------------------------------+
* See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage.
*/
- public void startMonitoring(@NonNull final IpServer ipServer) {
+ private void startConntrackMonitoring() {
// TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim.
- if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+ if (!mDeps.isAtLeastS()) return;
- if (mMonitoringIpServers.contains(ipServer)) {
- Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
- + " should not start monitoring twice.");
- return;
- }
-
- if (mMonitoringIpServers.isEmpty()) {
- mConntrackMonitor.start();
- mLog.i("Monitoring started");
- }
-
- mMonitoringIpServers.add(ipServer);
+ mConntrackMonitor.start();
+ mLog.i("Conntrack monitoring started.");
}
/**
* Stop conntrack event monitoring.
- * Note that this can be only called on handler thread.
*/
- public void stopMonitoring(@NonNull final IpServer ipServer) {
+ private void stopConntrackMonitoring() {
// TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim.
- if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
-
- // Ignore stopping monitoring if the monitor has never started for a given IpServer.
- if (!mMonitoringIpServers.contains(ipServer)) {
- mLog.e("Ignore stopping monitoring because monitoring has never started for "
- + ipServer.interfaceName());
- return;
- }
-
- mMonitoringIpServers.remove(ipServer);
-
- if (!mMonitoringIpServers.isEmpty()) return;
+ if (!mDeps.isAtLeastS()) return;
mConntrackMonitor.stop();
- mLog.i("Monitoring stopped");
+ mLog.i("Conntrack monitoring stopped.");
}
/**
@@ -688,9 +715,8 @@
/**
* Add IPv6 downstream rule.
- * Note that this can be only called on handler thread.
*/
- public void addIpv6DownstreamRule(
+ private void addIpv6DownstreamRule(
@NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
if (!isUsingBpf()) return;
@@ -706,9 +732,8 @@
/**
* Remove IPv6 downstream rule.
- * Note that this can be only called on handler thread.
*/
- public void removeIpv6DownstreamRule(
+ private void removeIpv6DownstreamRule(
@NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
if (!isUsingBpf()) return;
@@ -762,9 +787,8 @@
/**
* Delete all upstream and downstream rules for the passed-in IpServer, and if the new upstream
* is nonzero, reapply them to the new upstream.
- * Note that this can be only called on handler thread.
*/
- public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
+ private void updateAllIpv6Rules(@NonNull final IpServer ipServer,
final InterfaceParams interfaceParams, int newUpstreamIfindex,
@NonNull final Set<IpPrefix> newUpstreamPrefixes) {
if (!isUsingBpf()) return;
@@ -886,6 +910,141 @@
}
/**
+ * Register an IpServer (downstream).
+ * Note that this can be only called on handler thread.
+ */
+ public void addIpServer(@NonNull final IpServer ipServer) {
+ if (!isUsingBpf()) return;
+ if (mServedIpServers.contains(ipServer)) {
+ Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
+ + " should not add twice.");
+ return;
+ }
+
+ // Start monitoring and polling when the first IpServer is added.
+ if (mServedIpServers.isEmpty()) {
+ startStatsAndConntrackPolling();
+ startConntrackMonitoring();
+ mIpNeighborMonitor.start();
+ mLog.i("Neighbor monitoring started.");
+ }
+ mServedIpServers.add(ipServer);
+ }
+
+ /**
+ * Unregister an IpServer (downstream).
+ * Note that this can be only called on handler thread.
+ */
+ public void removeIpServer(@NonNull final IpServer ipServer) {
+ if (!isUsingBpf()) return;
+ if (!mServedIpServers.contains(ipServer)) {
+ mLog.e("Ignore removing because IpServer has never started for "
+ + ipServer.interfaceName());
+ return;
+ }
+ mServedIpServers.remove(ipServer);
+
+ // Stop monitoring and polling when the last IpServer is removed.
+ if (mServedIpServers.isEmpty()) {
+ stopStatsAndConntrackPolling();
+ stopConntrackMonitoring();
+ mIpNeighborMonitor.stop();
+ mLog.i("Neighbor monitoring stopped.");
+ }
+ }
+
+ /**
+ * Update upstream interface and its prefixes.
+ * Note that this can be only called on handler thread.
+ */
+ public void updateIpv6UpstreamInterface(@NonNull final IpServer ipServer, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes) {
+ if (!isUsingBpf()) return;
+
+ // If the upstream interface has changed, remove all rules and re-add them with the new
+ // upstream interface. If upstream is a virtual network, treated as no upstream.
+ final int prevUpstreamIfindex = ipServer.getIpv6UpstreamIfindex();
+ final InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+ final Set<IpPrefix> prevUpstreamPrefixes = ipServer.getIpv6UpstreamPrefixes();
+ if (prevUpstreamIfindex != upstreamIfindex
+ || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
+ final boolean upstreamSupportsBpf = checkUpstreamSupportsBpf(upstreamIfindex);
+ updateAllIpv6Rules(ipServer, interfaceParams,
+ getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
+ upstreamPrefixes);
+ }
+ }
+
+ private boolean checkUpstreamSupportsBpf(int upstreamIfindex) {
+ final String iface = mInterfaceNames.get(upstreamIfindex);
+ return iface != null && !isVcnInterface(iface);
+ }
+
+ private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
+ return supportsBpf ? ifindex : NO_UPSTREAM;
+ }
+
+ // Handles updates to IPv6 downstream rules if a neighbor event is received.
+ private void addOrRemoveIpv6Downstream(@NonNull IpServer ipServer, NeighborEvent e) {
+ // mInterfaceParams must be non-null or the event would not have arrived.
+ if (e == null) return;
+ if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
+ || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+ return;
+ }
+
+ // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
+ // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
+ // never add rules with a null MAC, only delete them.
+ final InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+ if (interfaceParams == null || interfaceParams.macAddr == null) return;
+ final int lastIpv6UpstreamIfindex = ipServer.getIpv6UpstreamIfindex();
+ final boolean isUpstreamSupportsBpf = checkUpstreamSupportsBpf(lastIpv6UpstreamIfindex);
+ MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+ Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
+ getInterfaceIndexForRule(lastIpv6UpstreamIfindex, isUpstreamSupportsBpf),
+ interfaceParams.index, (Inet6Address) e.ip, interfaceParams.macAddr, dstMac);
+ if (e.isValid()) {
+ addIpv6DownstreamRule(ipServer, rule);
+ } else {
+ removeIpv6DownstreamRule(ipServer, rule);
+ }
+ }
+
+ private void updateClientInfoIpv4(@NonNull IpServer ipServer, NeighborEvent e) {
+ if (e == null) return;
+ if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
+ || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+ return;
+ }
+
+ InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+ if (interfaceParams == null) return;
+
+ // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
+ // ignored. Do this here instead of in the ClientInfo constructor to ensure that
+ // IpServer never add clients with a null MAC, only delete them.
+ final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+ final ClientInfo clientInfo = new ClientInfo(interfaceParams.index,
+ interfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
+ if (e.isValid()) {
+ tetherOffloadClientAdd(ipServer, clientInfo);
+ } else {
+ tetherOffloadClientRemove(ipServer, clientInfo);
+ }
+ }
+
+ private void handleNeighborEvent(@NonNull IpServer ipServer, NeighborEvent e) {
+ InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+ if (interfaceParams != null
+ && interfaceParams.index == e.ifindex
+ && interfaceParams.hasMacAddress) {
+ addOrRemoveIpv6Downstream(ipServer, e);
+ updateClientInfoIpv4(ipServer, e);
+ }
+ }
+
+ /**
* Clear all forwarding IPv4 rules for a given client.
* Note that this can be only called on handler thread.
*/
@@ -1136,7 +1295,7 @@
// Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
// "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
- pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
+ pw.println("Polling " + (mServedIpServers.isEmpty() ? "not started" : "started"));
pw.println("Stats provider " + (mStatsProvider != null
? "registered" : "not registered"));
pw.println("Upstream quota: " + mInterfaceQuotas.toString());
@@ -1197,6 +1356,9 @@
pw.increaseIndent();
dumpCounters(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("mSupportActiveSessionsMetrics: " + mSupportActiveSessionsMetrics);
}
private void dumpStats(@NonNull IndentingPrintWriter pw) {
@@ -2033,8 +2195,31 @@
maybeAddDevMap(upstreamIndex, tetherClient.downstreamIfindex);
maybeSetLimit(upstreamIndex);
- mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value);
- mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value);
+
+ final boolean addedUpstream = mBpfCoordinatorShim.tetherOffloadRuleAdd(
+ UPSTREAM, upstream4Key, upstream4Value);
+ final boolean addedDownstream = mBpfCoordinatorShim.tetherOffloadRuleAdd(
+ DOWNSTREAM, downstream4Key, downstream4Value);
+ if (addedUpstream != addedDownstream) {
+ Log.wtf(TAG, "The bidirectional rules should be added concurrently ("
+ + "upstream: " + addedUpstream
+ + ", downstream: " + addedDownstream + ")");
+ return;
+ }
+ }
+ }
+
+ @VisibleForTesting(visibility = PRIVATE)
+ public int getLastMaxConnectionAndResetToCurrent() {
+ return mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent();
+ }
+
+ @VisibleForTesting
+ private class BpfNeighborEventConsumer implements NeighborEventConsumer {
+ public void accept(NeighborEvent e) {
+ for (IpServer ipServer : mServedIpServers) {
+ handleNeighborEvent(ipServer, e);
+ }
}
}
@@ -2365,9 +2550,12 @@
});
}
- private void maybeSchedulePollingStats() {
- if (!mPollingStarted) return;
+ private void uploadConntrackMetricsSample() {
+ mDeps.sendTetheringActiveSessionsReported(
+ mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent());
+ }
+ private void schedulePollingStats() {
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
}
@@ -2375,9 +2563,7 @@
mHandler.postDelayed(mScheduledPollingStats, getPollingInterval());
}
- private void maybeScheduleConntrackTimeoutUpdate() {
- if (!mPollingStarted) return;
-
+ private void scheduleConntrackTimeoutUpdate() {
if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
}
@@ -2386,6 +2572,15 @@
CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
}
+ private void scheduleConntrackMetricsSampling() {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+
+ mHandler.postDelayed(mScheduledConntrackMetricsSampling,
+ CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ }
+
// Return IPv6 downstream forwarding rule map. This is used for testing only.
// Note that this can be only called on handler thread.
@NonNull
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 528991f..1d5df61 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -28,11 +28,11 @@
import static java.util.Arrays.asList;
-import android.content.Context;
-import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
+import android.net.LinkProperties;
import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.ip.IpServer;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -51,6 +51,7 @@
import java.util.List;
import java.util.Random;
import java.util.Set;
+import java.util.function.Supplier;
/**
* This class coordinate IP addresses conflict problem.
@@ -63,6 +64,7 @@
* @hide
*/
public class PrivateAddressCoordinator {
+ // WARNING: Keep in sync with chooseDownstreamAddress
public static final int PREFIX_LENGTH = 24;
// Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
@@ -75,18 +77,22 @@
private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
private static final String LEGACY_BLUETOOTH_IFACE_ADDRESS = "192.168.44.1/24";
private final List<IpPrefix> mTetheringPrefixes;
- private final ConnectivityManager mConnectivityMgr;
- private final TetheringConfiguration mConfig;
+ // A supplier that returns ConnectivityManager#getAllNetworks.
+ private final Supplier<Network[]> mGetAllNetworksSupplier;
+ private final boolean mIsRandomPrefixBaseEnabled;
+ private final boolean mShouldEnableWifiP2pDedicatedIp;
// keyed by downstream type(TetheringManager.TETHERING_*).
private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
private final Random mRandom;
- public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
+ public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier,
+ boolean isRandomPrefixBase,
+ boolean shouldEnableWifiP2pDedicatedIp) {
mDownstreams = new ArraySet<>();
mUpstreamPrefixMap = new ArrayMap<>();
- mConnectivityMgr = (ConnectivityManager) context.getSystemService(
- Context.CONNECTIVITY_SERVICE);
- mConfig = config;
+ mGetAllNetworksSupplier = getAllNetworksSupplier;
+ mIsRandomPrefixBaseEnabled = isRandomPrefixBase;
+ mShouldEnableWifiP2pDedicatedIp = shouldEnableWifiP2pDedicatedIp;
mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
// Reserved static addresses for bluetooth and wifi p2p.
mCachedAddresses.put(new AddressKey(TETHERING_BLUETOOTH, CONNECTIVITY_SCOPE_GLOBAL),
@@ -100,26 +106,26 @@
}
/**
- * Record a new upstream IpPrefix which may conflict with tethering downstreams.
- * The downstreams will be notified if a conflict is found. When updateUpstreamPrefix is called,
+ * Record a new upstream IpPrefix which may conflict with tethering downstreams. The downstreams
+ * will be notified if a conflict is found. When updateUpstreamPrefix is called,
* UpstreamNetworkState must have an already populated LinkProperties.
*/
- public void updateUpstreamPrefix(final UpstreamNetworkState ns) {
+ public void updateUpstreamPrefix(
+ final LinkProperties lp, final NetworkCapabilities nc, final Network network) {
// Do not support VPN as upstream. Normally, networkCapabilities is not expected to be null,
// but just checking to be sure.
- if (ns.networkCapabilities != null && ns.networkCapabilities.hasTransport(TRANSPORT_VPN)) {
- removeUpstreamPrefix(ns.network);
+ if (nc != null && nc.hasTransport(TRANSPORT_VPN)) {
+ removeUpstreamPrefix(network);
return;
}
- final ArrayList<IpPrefix> ipv4Prefixes = getIpv4Prefixes(
- ns.linkProperties.getAllLinkAddresses());
+ final ArrayList<IpPrefix> ipv4Prefixes = getIpv4Prefixes(lp.getAllLinkAddresses());
if (ipv4Prefixes.isEmpty()) {
- removeUpstreamPrefix(ns.network);
+ removeUpstreamPrefix(network);
return;
}
- mUpstreamPrefixMap.put(ns.network, ipv4Prefixes);
+ mUpstreamPrefixMap.put(network, ipv4Prefixes);
handleMaybePrefixConflict(ipv4Prefixes);
}
@@ -161,7 +167,7 @@
// Remove all upstreams that are no longer valid networks
final Set<Network> toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet());
- toBeRemoved.removeAll(asList(mConnectivityMgr.getAllNetworks()));
+ toBeRemoved.removeAll(asList(mGetAllNetworksSupplier.get()));
mUpstreamPrefixMap.removeAll(toBeRemoved);
}
@@ -173,7 +179,7 @@
@Nullable
public LinkAddress requestDownstreamAddress(final IpServer ipServer, final int scope,
boolean useLastAddress) {
- if (mConfig.shouldEnableWifiP2pDedicatedIp()
+ if (mShouldEnableWifiP2pDedicatedIp
&& ipServer.interfaceType() == TETHERING_WIFI_P2P) {
return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
}
@@ -189,7 +195,7 @@
return cachedAddress;
}
- final int prefixIndex = getStartedPrefixIndex();
+ final int prefixIndex = getRandomPrefixIndex();
for (int i = 0; i < mTetheringPrefixes.size(); i++) {
final IpPrefix prefixRange = mTetheringPrefixes.get(
(prefixIndex + i) % mTetheringPrefixes.size());
@@ -205,8 +211,8 @@
return null;
}
- private int getStartedPrefixIndex() {
- if (!mConfig.isRandomPrefixBaseEnabled()) return 0;
+ private int getRandomPrefixIndex() {
+ if (!mIsRandomPrefixBaseEnabled) return 0;
final int random = getRandomInt() & 0xffffff;
// This is to select the starting prefix range (/8, /12, or /16) instead of the actual
@@ -242,123 +248,62 @@
return getInUseDownstreamPrefix(prefix);
}
- // Get the next non-conflict sub prefix. E.g: To get next sub prefix from 10.0.0.0/8, if the
- // previously selected prefix is 10.20.42.0/24(subPrefix: 0.20.42.0) and the conflicting prefix
- // is 10.16.0.0/20 (10.16.0.0 ~ 10.16.15.255), then the max address under subPrefix is
- // 0.16.15.255 and the next subPrefix is 0.16.16.255/24 (0.16.15.255 + 0.0.1.0).
- // Note: the sub address 0.0.0.255 here is fine to be any value that it will be replaced as
- // selected random sub address later.
- private int getNextSubPrefix(final IpPrefix conflictPrefix, final int prefixRangeMask) {
- final int suffixMask = ~prefixLengthToV4NetmaskIntHTH(conflictPrefix.getPrefixLength());
- // The largest offset within the prefix assignment block that still conflicts with
- // conflictPrefix.
- final int maxConflict =
- (getPrefixBaseAddress(conflictPrefix) | suffixMask) & ~prefixRangeMask;
-
- final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
- // Pick a sub prefix a full prefix (1 << (32 - PREFIX_LENGTH) addresses) greater than
- // maxConflict. This ensures that the selected prefix never overlaps with conflictPrefix.
- // There is no need to mask the result with PREFIX_LENGTH bits because this is done by
- // findAvailablePrefixFromRange when it constructs the prefix.
- return maxConflict + (1 << (32 - PREFIX_LENGTH));
- }
-
- private LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
+ @VisibleForTesting
+ public LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
// The netmask of the prefix assignment block (e.g., 0xfff00000 for 172.16.0.0/12).
final int prefixRangeMask = prefixLengthToV4NetmaskIntHTH(prefixRange.getPrefixLength());
// The zero address in the block (e.g., 0xac100000 for 172.16.0.0/12).
final int baseAddress = getPrefixBaseAddress(prefixRange);
- // The subnet mask corresponding to PREFIX_LENGTH.
- final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
+ // Try to get an address within the given prefix that does not conflict with any other
+ // prefix in the system.
+ for (int i = 0; i < 20; ++i) {
+ final int randomSuffix = mRandom.nextInt() & ~prefixRangeMask;
+ final int randomAddress = baseAddress | randomSuffix;
- // The offset within prefixRange of a randomly-selected prefix of length PREFIX_LENGTH.
- // This may not be the prefix of the address returned by this method:
- // - If it is already in use, the method will return an address in another prefix.
- // - If all prefixes within prefixRange are in use, the method will return null. For
- // example, for a /24 prefix within 172.26.0.0/12, this will be a multiple of 256 in
- // [0, 1048576). In other words, a random 32-bit number with mask 0x000fff00.
- //
- // prefixRangeMask is required to ensure no wrapping. For example, consider:
- // - prefixRange 127.0.0.0/8
- // - randomPrefixStart 127.255.255.0
- // - A conflicting prefix of 127.255.254.0/23
- // In this case without prefixRangeMask, getNextSubPrefix would return 128.0.0.0, which
- // means the "start < end" check in findAvailablePrefixFromRange would not reject the prefix
- // because Java doesn't have unsigned integers, so 128.0.0.0 = 0x80000000 = -2147483648
- // is less than 127.0.0.0 = 0x7f000000 = 2130706432.
- //
- // Additionally, it makes debug output easier to read by making the numbers smaller.
- final int randomInt = getRandomInt();
- final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
+ // Avoid selecting x.x.x.[0, 1, 255] addresses.
+ switch (randomAddress & 0xFF) {
+ case 0:
+ case 1:
+ case 255:
+ // Try selecting a different address
+ continue;
+ }
- // A random offset within the prefix. Used to determine the local address once the prefix
- // is selected. It does not result in an IPv4 address ending in .0, .1, or .255
- // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
- final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
+ // Avoid selecting commonly used subnets.
+ switch (randomAddress & 0xFFFFFF00) {
+ case 0xC0A80000: // 192.168.0.0/24
+ case 0xC0A80100: // 192.168.1.0/24
+ case 0xC0A85800: // 192.168.88.0/24
+ case 0xC0A86400: // 192.168.100.0/24
+ continue;
+ }
- // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
- // such that the prefix does not conflict with any upstream.
- IpPrefix downstreamPrefix = findAvailablePrefixFromRange(
- randomPrefixStart, (~prefixRangeMask) + 1, baseAddress, prefixRangeMask);
- if (downstreamPrefix != null) return getLinkAddress(downstreamPrefix, subAddress);
+ // Avoid 10.0.0.0 - 10.10.255.255
+ if (randomAddress >= 0x0A000000 && randomAddress <= 0x0A0AFFFF) {
+ continue;
+ }
- // If that failed, do the same, but between 0 and randomPrefixStart.
- downstreamPrefix = findAvailablePrefixFromRange(
- 0, randomPrefixStart, baseAddress, prefixRangeMask);
-
- return getLinkAddress(downstreamPrefix, subAddress);
- }
-
- private LinkAddress getLinkAddress(final IpPrefix prefix, final int subAddress) {
- if (prefix == null) return null;
-
- final InetAddress address = intToInet4AddressHTH(getPrefixBaseAddress(prefix) | subAddress);
- return new LinkAddress(address, PREFIX_LENGTH);
- }
-
- private IpPrefix findAvailablePrefixFromRange(final int start, final int end,
- final int baseAddress, final int prefixRangeMask) {
- int newSubPrefix = start;
- while (newSubPrefix < end) {
- final InetAddress address = intToInet4AddressHTH(baseAddress | newSubPrefix);
+ final InetAddress address = intToInet4AddressHTH(randomAddress);
final IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH);
-
- final IpPrefix conflictPrefix = getConflictPrefix(prefix);
-
- if (conflictPrefix == null) return prefix;
-
- newSubPrefix = getNextSubPrefix(conflictPrefix, prefixRangeMask);
+ if (getConflictPrefix(prefix) != null) {
+ // Prefix is conflicting with another prefix used in the system, find another one.
+ continue;
+ }
+ return new LinkAddress(address, PREFIX_LENGTH);
}
-
+ // Could not find a prefix, return null and let caller try another range.
return null;
}
/** Get random int which could be used to generate random address. */
+ // TODO: get rid of this function and mock getRandomPrefixIndex in tests.
@VisibleForTesting
public int getRandomInt() {
return mRandom.nextInt();
}
- /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
- private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
- final int randomSubAddr = randomInt & subAddrMask;
- // If prefix length > 30, the selecting speace would be less than 4 which may be hard to
- // avoid 3 consecutive address.
- if (PREFIX_LENGTH > 30) return randomSubAddr;
-
- // TODO: maybe it is not necessary to avoid .0, .1 and .255 address because tethering
- // address would not be conflicted. This code only works because PREFIX_LENGTH is not longer
- // than 24
- final int candidate = randomSubAddr & 0xff;
- if (candidate == 0 || candidate == 1 || candidate == 255) {
- return (randomSubAddr & 0xfffffffc) + 2;
- }
-
- return randomSubAddr;
- }
-
/** Release downstream record for IpServer. */
public void releaseDownstream(final IpServer ipServer) {
mDownstreams.remove(ipServer);
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index d85d92f..49bc86e 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -38,6 +38,7 @@
import static android.net.TetheringManager.TETHERING_INVALID;
import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHERING_WIGIG;
@@ -90,14 +91,12 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
-import android.net.RoutingCoordinatorManager;
import android.net.TetherStatesParcel;
import android.net.TetheredClient;
import android.net.TetheringCallbackStartedParcel;
import android.net.TetheringConfigurationParcel;
import android.net.TetheringInterface;
import android.net.TetheringManager.TetheringRequest;
-import android.net.TetheringRequestParcel;
import android.net.Uri;
import android.net.ip.IpServer;
import android.net.wifi.WifiClient;
@@ -138,7 +137,7 @@
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.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -147,7 +146,6 @@
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
-import com.android.networkstack.tethering.util.TetheringUtils;
import com.android.networkstack.tethering.util.VersionedBroadcastListener;
import com.android.networkstack.tethering.wear.WearableConnectionManager;
@@ -231,7 +229,7 @@
// Currently active tethering requests per tethering type. Only one of each type can be
// requested at a time. After a tethering type is requested, the map keeps tethering parameters
// to be used after the interface comes up asynchronously.
- private final SparseArray<TetheringRequestParcel> mActiveTetheringRequests =
+ private final SparseArray<TetheringRequest> mActiveTetheringRequests =
new SparseArray<>();
private final Context mContext;
@@ -247,10 +245,7 @@
private final Handler mHandler;
private final INetd mNetd;
private final NetdCallback mNetdCallback;
- // Contains null if the connectivity module is unsupported, as the routing coordinator is not
- // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
- // must be able to find all classes at runtime.
- @NonNull private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+ private final RoutingCoordinatorManager mRoutingCoordinator;
private final UserRestrictionActionListener mTetheringRestriction;
private final ActiveDataSubIdListener mActiveDataSubIdListener;
private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -278,6 +273,7 @@
private TetheredInterfaceRequestShim mBluetoothIfaceRequest;
private String mConfiguredEthernetIface;
private String mConfiguredBluetoothIface;
+ private String mConfiguredVirtualIface;
private EthernetCallback mEthernetCallback;
private TetheredInterfaceCallbackShim mBluetoothCallback;
private SettingsObserver mSettingsObserver;
@@ -294,11 +290,11 @@
mLog.mark("Tethering.constructed");
mDeps = deps;
mContext = mDeps.getContext();
- mNetd = mDeps.getINetd(mContext);
- mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
+ mNetd = mDeps.getINetd(mContext, mLog);
+ mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext, mLog);
mLooper = mDeps.makeTetheringLooper();
mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
- mTetheringMetrics = mDeps.makeTetheringMetrics();
+ mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
// This is intended to ensrure that if something calls startTethering(bluetooth) just after
// bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -378,6 +374,11 @@
}
@NonNull
+ public Context getContext() {
+ return mContext;
+ }
+
+ @NonNull
public INetd getNetd() {
return mNetd;
}
@@ -659,28 +660,27 @@
processInterfaceStateChange(iface, false /* enabled */);
}
- void startTethering(final TetheringRequestParcel request, final String callerPkg,
+ void startTethering(final TetheringRequest request, final String callerPkg,
final IIntResultListener listener) {
mHandler.post(() -> {
- final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
- request.tetheringType);
+ final int type = request.getTetheringType();
+ final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
// If tethering is already enabled with a different request,
// disable before re-enabling.
- if (unfinishedRequest != null
- && !TetheringUtils.isTetheringRequestEquals(unfinishedRequest, request)) {
- enableTetheringInternal(request.tetheringType, false /* disabled */, null);
- mEntitlementMgr.stopProvisioningIfNeeded(request.tetheringType);
+ if (unfinishedRequest != null && !unfinishedRequest.equals(request)) {
+ enableTetheringInternal(type, false /* disabled */, null);
+ mEntitlementMgr.stopProvisioningIfNeeded(type);
}
- mActiveTetheringRequests.put(request.tetheringType, request);
+ mActiveTetheringRequests.put(type, request);
- if (request.exemptFromEntitlementCheck) {
- mEntitlementMgr.setExemptedDownstreamType(request.tetheringType);
+ if (request.isExemptFromEntitlementCheck()) {
+ mEntitlementMgr.setExemptedDownstreamType(type);
} else {
- mEntitlementMgr.startProvisioningIfNeeded(request.tetheringType,
- request.showProvisioningUi);
+ mEntitlementMgr.startProvisioningIfNeeded(type,
+ request.getShouldShowEntitlementUi());
}
- enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
- mTetheringMetrics.createBuilder(request.tetheringType, callerPkg);
+ enableTetheringInternal(type, true /* enabled */, listener);
+ mTetheringMetrics.createBuilder(type, callerPkg);
});
}
@@ -719,6 +719,9 @@
case TETHERING_ETHERNET:
result = setEthernetTethering(enable);
break;
+ case TETHERING_VIRTUAL:
+ result = setVirtualMachineTethering(enable);
+ break;
default:
Log.w(TAG, "Invalid tether type.");
result = TETHER_ERROR_UNKNOWN_TYPE;
@@ -972,6 +975,21 @@
}
}
+ private int setVirtualMachineTethering(final boolean enable) {
+ // TODO(340377643): Use bridge ifname when it's introduced, not fixed TAP ifname.
+ if (enable) {
+ mConfiguredVirtualIface = "avf_tap_fixed";
+ enableIpServing(
+ TETHERING_VIRTUAL,
+ mConfiguredVirtualIface,
+ getRequestedState(TETHERING_VIRTUAL));
+ } else if (mConfiguredVirtualIface != null) {
+ ensureIpServerStopped(mConfiguredVirtualIface);
+ mConfiguredVirtualIface = null;
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
void tether(String iface, int requestedState, final IIntResultListener listener) {
mHandler.post(() -> {
try {
@@ -998,7 +1016,7 @@
//
// This code cannot race with untether() because they both run on the handler thread.
final int type = tetherState.ipServer.interfaceType();
- final TetheringRequestParcel request = mActiveTetheringRequests.get(type, null);
+ final TetheringRequest request = mActiveTetheringRequests.get(type, null);
if (request != null) {
mActiveTetheringRequests.delete(type);
}
@@ -1055,14 +1073,14 @@
}
private int getRequestedState(int type) {
- final TetheringRequestParcel request = mActiveTetheringRequests.get(type);
+ final TetheringRequest request = mActiveTetheringRequests.get(type);
// The request could have been deleted before we had a chance to complete it.
// If so, assume that the scope is the default scope for this tethering type.
// This likely doesn't matter - if the request has been deleted, then tethering is
// likely going to be stopped soon anyway.
final int connectivityScope = (request != null)
- ? request.connectivityScope
+ ? request.getConnectivityScope()
: TetheringRequest.getDefaultConnectivityScope(type);
return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
@@ -1361,7 +1379,7 @@
}
@VisibleForTesting
- SparseArray<TetheringRequestParcel> getActiveTetheringRequests() {
+ SparseArray<TetheringRequest> getActiveTetheringRequests() {
return mActiveTetheringRequests;
}
@@ -1986,7 +2004,8 @@
final UpstreamNetworkState ns = (UpstreamNetworkState) o;
switch (arg1) {
case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
- mPrivateAddressCoordinator.updateUpstreamPrefix(ns);
+ mPrivateAddressCoordinator.updateUpstreamPrefix(
+ ns.linkProperties, ns.networkCapabilities, ns.network);
break;
case UpstreamNetworkMonitor.EVENT_ON_LOST:
mPrivateAddressCoordinator.removeUpstreamPrefix(ns.network);
@@ -2069,9 +2088,7 @@
chooseUpstreamType(true);
mTryCell = false;
}
-
- // TODO: Check the upstream interface if it is managed by BPF offload.
- mBpfCoordinator.startPolling();
+ mTetheringMetrics.initUpstreamUsageBaseline();
}
@Override
@@ -2085,7 +2102,6 @@
reportUpstreamChanged(null);
mNotificationUpdater.onUpstreamCapabilitiesChanged(null);
}
- mBpfCoordinator.stopPolling();
mTetheringMetrics.cleanup();
}
@@ -2384,9 +2400,6 @@
hasCallingPermission(NETWORK_SETTINGS)
|| hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
|| hasCallingPermission(NETWORK_STACK);
- if (callback == null) {
- throw new NullPointerException();
- }
mHandler.post(() -> {
mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
@@ -2424,9 +2437,6 @@
/** Unregister tethering event callback */
void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
- if (callback == null) {
- throw new NullPointerException();
- }
mHandler.post(() -> {
mTetheringEventCallbacks.unregister(callback);
});
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 298940e..c9817c9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -144,6 +144,12 @@
/** A flag for using synchronous or asynchronous state machine. */
public static boolean USE_SYNC_SM = false;
+ /**
+ * A feature flag to control whether the active sessions metrics should be enabled.
+ * Disabled by default.
+ */
+ public static final String TETHER_ACTIVE_SESSIONS_METRICS = "tether_active_sessions_metrics";
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 3f86056..81f057c 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -21,8 +21,8 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothPan;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.INetd;
-import android.net.RoutingCoordinatorManager;
import android.net.connectivity.ConnectivityInternalApiUtil;
import android.net.ip.IpServer;
import android.os.Build;
@@ -36,7 +36,8 @@
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
+import com.android.net.module.util.RoutingCoordinatorService;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.BluetoothPanShimImpl;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -120,19 +121,26 @@
/**
* Get a reference to INetd to be used by tethering.
*/
- public INetd getINetd(Context context) {
- return INetd.Stub.asInterface(
- (IBinder) context.getSystemService(Context.NETD_SERVICE));
+ public INetd getINetd(Context context, SharedLog log) {
+ final INetd netd =
+ INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+ if (netd == null) {
+ log.wtf("INetd is null");
+ }
+ return netd;
}
/**
- * Get the routing coordinator, or null if below S.
+ * Get the routing coordinator.
*/
- @Nullable
- public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
- if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
- return new LateSdk<>(
- ConnectivityInternalApiUtil.getRoutingCoordinatorManager(context));
+ public RoutingCoordinatorManager getRoutingCoordinator(Context context, SharedLog log) {
+ IBinder binder;
+ if (!SdkLevel.isAtLeastS()) {
+ binder = new RoutingCoordinatorService(getINetd(context, log));
+ } else {
+ binder = ConnectivityInternalApiUtil.getRoutingCoordinator(context);
+ }
+ return new RoutingCoordinatorManager(context, binder);
}
/**
@@ -169,9 +177,13 @@
/**
* Make PrivateAddressCoordinator to be used by Tethering.
*/
- public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
- TetheringConfiguration cfg) {
- return new PrivateAddressCoordinator(ctx, cfg);
+ public PrivateAddressCoordinator makePrivateAddressCoordinator(
+ Context ctx, TetheringConfiguration cfg) {
+ final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+ return new PrivateAddressCoordinator(
+ cm::getAllNetworks,
+ cfg.isRandomPrefixBaseEnabled(),
+ cfg.shouldEnableWifiP2pDedicatedIp());
}
/**
@@ -186,8 +198,8 @@
/**
* Make the TetheringMetrics to be used by tethering.
*/
- public TetheringMetrics makeTetheringMetrics() {
- return new TetheringMetrics();
+ public TetheringMetrics makeTetheringMetrics(Context ctx) {
+ return new TetheringMetrics(ctx);
}
/**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 623f502..454cbf1 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -38,6 +38,7 @@
import android.net.ITetheringConnector;
import android.net.ITetheringEventCallback;
import android.net.NetworkStack;
+import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringRequestParcel;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
@@ -137,8 +138,8 @@
listener)) {
return;
}
-
- mTethering.startTethering(request, callerPkg, listener);
+ // TODO(b/216524590): Add UID/packageName of caller to TetheringRequest here
+ mTethering.startTethering(new TetheringRequest(request), callerPkg, listener);
}
@Override
@@ -163,6 +164,8 @@
@Override
public void registerTetheringEventCallback(ITetheringEventCallback callback,
String callerPkg) {
+ // Silently ignore call if the callback is null. This can only happen via reflection.
+ if (callback == null) return;
try {
if (!hasTetherAccessPermission()) {
callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
@@ -175,6 +178,8 @@
@Override
public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
String callerPkg) {
+ // Silently ignore call if the callback is null. This can only happen via reflection.
+ if (callback == null) return;
try {
if (!hasTetherAccessPermission()) {
callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 814afcd..6de4062 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -16,12 +16,21 @@
package com.android.networkstack.tethering.metrics;
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_NCM;
@@ -46,17 +55,26 @@
import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
import android.annotation.Nullable;
+import android.app.usage.NetworkStats;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.stats.connectivity.DownstreamType;
import android.stats.connectivity.ErrorCode;
import android.stats.connectivity.UpstreamType;
import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.networkstack.tethering.UpstreamNetworkState;
import java.util.ArrayList;
@@ -65,6 +83,10 @@
/**
* Collection of utilities for tethering metrics.
*
+ * <p>This class is thread-safe. All accesses to this class will be either posting to the internal
+ * handler thread for processing or checking whether the access is from the internal handler
+ * thread. However, the constructor is an exception, as it is called on another thread.
+ *
* To see if the logs are properly sent to statsd, execute following commands
*
* $ adb shell cmd stats print-logs
@@ -78,31 +100,145 @@
private static final String SETTINGS_PKG_NAME = "com.android.settings";
private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
private static final String GMS_PKG_NAME = "com.google.android.gms";
+ /**
+ * A feature flag to control whether upstream data usage metrics should be enabled.
+ */
+ private static final String TETHER_UPSTREAM_DATA_USAGE_METRICS =
+ "tether_upstream_data_usage_metrics";
+ @VisibleForTesting
+ static final DataUsage EMPTY = new DataUsage(0L /* txBytes */, 0L /* rxBytes */);
private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
+ // Store the last reported data usage for each upstream type to be used for calculating the
+ // usage delta. The keys are the upstream types, and the values are the tethering UID data
+ // usage for the corresponding types. Retrieve the baseline data usage when tethering is
+ // enabled, update it when the upstream changes, and clear it when tethering is disabled.
+ private final ArrayMap<UpstreamType, DataUsage> mLastReportedUpstreamUsage = new ArrayMap<>();
+ private final Context mContext;
+ private final Dependencies mDependencies;
+ private final NetworkStatsManager mNetworkStatsManager;
+ private final Handler mHandler;
private UpstreamType mCurrentUpstream = null;
private Long mCurrentUpStreamStartTime = 0L;
+ /**
+ * Dependencies of TetheringMetrics, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * @see TetheringStatsLog
+ */
+ public void write(NetworkTetheringReported reported) {
+ TetheringStatsLog.write(
+ TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+ reported.getErrorCode().getNumber(),
+ reported.getDownstreamType().getNumber(),
+ reported.getUpstreamType().getNumber(),
+ reported.getUserType().getNumber(),
+ reported.getUpstreamEvents().toByteArray(),
+ reported.getDurationMillis());
+ }
+
+ /**
+ * @see System#currentTimeMillis()
+ */
+ public long timeNow() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * Indicates whether {@link #TETHER_UPSTREAM_DATA_USAGE_METRICS} is enabled.
+ */
+ public boolean isUpstreamDataUsageMetricsEnabled(Context context) {
+ // Getting data usage requires building a NetworkTemplate. However, the
+ // NetworkTemplate#Builder API was introduced in Android T.
+ return SdkLevel.isAtLeastT() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ context, TETHER_UPSTREAM_DATA_USAGE_METRICS);
+ }
+
+ /**
+ * @see Handler
+ *
+ * Note: This should only be called once, within the constructor, as it creates a new
+ * thread. Calling it multiple times could lead to a thread leak.
+ */
+ @NonNull
+ public Handler createHandler() {
+ final HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ return new Handler(thread.getLooper());
+ }
+ }
/**
- * Return the current system time in milliseconds.
- * @return the current system time in milliseconds.
+ * Constructor for the TetheringMetrics class.
+ *
+ * @param context The Context object used to access system services.
*/
- public long timeNow() {
- return System.currentTimeMillis();
+ public TetheringMetrics(Context context) {
+ this(context, new Dependencies());
+ }
+
+ TetheringMetrics(Context context, Dependencies dependencies) {
+ mContext = context;
+ mDependencies = dependencies;
+ mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+ mHandler = dependencies.createHandler();
+ }
+
+ @VisibleForTesting
+ static class DataUsage {
+ public final long txBytes;
+ public final long rxBytes;
+
+ DataUsage(long txBytes, long rxBytes) {
+ this.txBytes = txBytes;
+ this.rxBytes = rxBytes;
+ }
+
+ /*** Calculate the data usage delta from give new and old usage */
+ public static DataUsage subtract(DataUsage newUsage, DataUsage oldUsage) {
+ return new DataUsage(
+ newUsage.txBytes - oldUsage.txBytes,
+ newUsage.rxBytes - oldUsage.rxBytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (txBytes & 0xFFFFFFFF)
+ + ((int) (txBytes >> 32) * 3)
+ + ((int) (rxBytes & 0xFFFFFFFF) * 5)
+ + ((int) (rxBytes >> 32) * 7);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DataUsage)) {
+ return false;
+ }
+ return txBytes == ((DataUsage) other).txBytes
+ && rxBytes == ((DataUsage) other).rxBytes;
+ }
+
}
private static class RecordUpstreamEvent {
- public final long mStartTime;
- public final long mStopTime;
- public final UpstreamType mUpstreamType;
+ final long mStartTime;
+ final long mStopTime;
+ final UpstreamType mUpstreamType;
+ final DataUsage mDataUsage;
RecordUpstreamEvent(final long startTime, final long stopTime,
- final UpstreamType upstream) {
+ final UpstreamType upstream, final DataUsage dataUsage) {
mStartTime = startTime;
mStopTime = stopTime;
mUpstreamType = upstream;
+ mDataUsage = dataUsage;
}
}
@@ -115,6 +251,10 @@
* @param callerPkg The package name of the caller.
*/
public void createBuilder(final int downstreamType, final String callerPkg) {
+ mHandler.post(() -> handleCreateBuilder(downstreamType, callerPkg));
+ }
+
+ private void handleCreateBuilder(final int downstreamType, final String callerPkg) {
NetworkTetheringReported.Builder statsBuilder = NetworkTetheringReported.newBuilder()
.setDownstreamType(downstreamTypeToEnum(downstreamType))
.setUserType(userTypeToEnum(callerPkg))
@@ -123,7 +263,7 @@
.setUpstreamEvents(UpstreamEvents.newBuilder())
.setDurationMillis(0);
mBuilderMap.put(downstreamType, statsBuilder);
- mDownstreamStartTime.put(downstreamType, timeNow());
+ mDownstreamStartTime.put(downstreamType, mDependencies.timeNow());
}
/**
@@ -132,6 +272,10 @@
* @param errCode The error code to set.
*/
public void updateErrorCode(final int downstreamType, final int errCode) {
+ mHandler.post(() -> handleUpdateErrorCode(downstreamType, errCode));
+ }
+
+ private void handleUpdateErrorCode(final int downstreamType, final int errCode) {
NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
if (statsBuilder == null) {
Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -141,18 +285,56 @@
}
/**
+ * Calculates the data usage difference between the current and previous usage for the
+ * specified upstream type.
+ *
+ * Note: This must be called before updating mCurrentUpstream when changing the upstream.
+ *
+ * @return A DataUsage object containing the calculated difference in transmitted (tx) and
+ * received (rx) bytes.
+ */
+ private DataUsage calculateDataUsageDelta(@Nullable UpstreamType upstream) {
+ if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+ return EMPTY;
+ }
+
+ if (upstream == null || !isUsageSupportedForUpstreamType(upstream)) {
+ return EMPTY;
+ }
+
+ final DataUsage oldUsage = mLastReportedUpstreamUsage.getOrDefault(upstream, EMPTY);
+ if (oldUsage.equals(EMPTY)) {
+ Log.d(TAG, "No usage baseline for the upstream=" + upstream);
+ return EMPTY;
+ }
+ // TODO(b/370724247): Fix data usage which might be incorrect if the device uses
+ // tethering with the same upstream for over 15 days.
+ // Need to refresh the baseline usage data. If the network switches back to Wi-Fi after
+ // using cellular data (Wi-Fi -> Cellular -> Wi-Fi), the old baseline might be
+ // inaccurate, leading to incorrect delta calculations.
+ final DataUsage newUsage = getCurrentDataUsageForUpstreamType(upstream);
+ mLastReportedUpstreamUsage.put(upstream, newUsage);
+ return DataUsage.subtract(newUsage, oldUsage);
+ }
+
+ /**
* Update the list of upstream types and their duration whenever the current upstream type
* changes.
* @param ns The UpstreamNetworkState object representing the current upstream network state.
*/
public void maybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
+ mHandler.post(() -> handleMaybeUpdateUpstreamType(ns));
+ }
+
+ private void handleMaybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
if (upstream.equals(mCurrentUpstream)) return;
- final long newTime = timeNow();
+ final long newTime = mDependencies.timeNow();
if (mCurrentUpstream != null) {
+ final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
- mCurrentUpstream));
+ mCurrentUpstream, dataUsage));
}
mCurrentUpstream = upstream;
mCurrentUpStreamStartTime = newTime;
@@ -203,13 +385,14 @@
final long startTime = Math.max(downstreamStartTime, event.mStartTime);
// Handle completed upstream events.
addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
- event.mUpstreamType, 0L /* txBytes */, 0L /* rxBytes */);
+ event.mUpstreamType, event.mDataUsage.txBytes, event.mDataUsage.rxBytes);
}
final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
- final long stopTime = timeNow();
+ final long stopTime = mDependencies.timeNow();
// Handle the last upstream event.
+ final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
- 0L /* txBytes */, 0L /* rxBytes */);
+ dataUsage.txBytes, dataUsage.rxBytes);
statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
}
@@ -225,6 +408,10 @@
* @param downstreamType the type of downstream event to remove statistics for
*/
public void sendReport(final int downstreamType) {
+ mHandler.post(() -> handleSendReport(downstreamType));
+ }
+
+ private void handleSendReport(final int downstreamType) {
final NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
if (statsBuilder == null) {
Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -245,18 +432,9 @@
*
* @param reported a NetworkTetheringReported object containing statistics to write
*/
- @VisibleForTesting
- public void write(@NonNull final NetworkTetheringReported reported) {
+ private void write(@NonNull final NetworkTetheringReported reported) {
final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
-
- TetheringStatsLog.write(
- TetheringStatsLog.NETWORK_TETHERING_REPORTED,
- reported.getErrorCode().getNumber(),
- reported.getDownstreamType().getNumber(),
- reported.getUpstreamType().getNumber(),
- reported.getUserType().getNumber(),
- upstreamEvents,
- reported.getDurationMillis());
+ mDependencies.write(reported);
if (DBG) {
Log.d(
TAG,
@@ -276,12 +454,71 @@
}
/**
+ * Initialize the upstream data usage baseline when tethering is turned on.
+ */
+ public void initUpstreamUsageBaseline() {
+ mHandler.post(() -> handleInitUpstreamUsageBaseline());
+ }
+
+ private void handleInitUpstreamUsageBaseline() {
+ if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+ return;
+ }
+
+ if (!mLastReportedUpstreamUsage.isEmpty()) {
+ Log.wtf(TAG, "The upstream usage baseline has been initialed.");
+ return;
+ }
+
+ for (UpstreamType type : UpstreamType.values()) {
+ if (!isUsageSupportedForUpstreamType(type)) continue;
+ mLastReportedUpstreamUsage.put(type, getCurrentDataUsageForUpstreamType(type));
+ }
+ }
+
+ @VisibleForTesting
+ @NonNull
+ DataUsage getLastReportedUsageFromUpstreamType(@NonNull UpstreamType type) {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ return mLastReportedUpstreamUsage.getOrDefault(type, EMPTY);
+ }
+
+
+ /**
+ * Get the current usage for given upstream type.
+ */
+ @NonNull
+ private DataUsage getCurrentDataUsageForUpstreamType(@NonNull UpstreamType type) {
+ final NetworkStats stats = mNetworkStatsManager.queryDetailsForUidTagState(
+ buildNetworkTemplateForUpstreamType(type), Long.MIN_VALUE, Long.MAX_VALUE,
+ UID_TETHERING, TAG_NONE, STATE_ALL);
+
+ final NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+ Long totalTxBytes = 0L;
+ Long totalRxBytes = 0L;
+ while (stats.hasNextBucket()) {
+ stats.getNextBucket(bucket);
+ totalTxBytes += bucket.getTxBytes();
+ totalRxBytes += bucket.getRxBytes();
+ }
+ return new DataUsage(totalTxBytes, totalRxBytes);
+ }
+
+ /**
* Cleans up the variables related to upstream events when tethering is turned off.
*/
public void cleanup() {
+ mHandler.post(() -> handleCleanup());
+ }
+
+ private void handleCleanup() {
mUpstreamEventList.clear();
mCurrentUpstream = null;
mCurrentUpStreamStartTime = 0L;
+ mLastReportedUpstreamUsage.clear();
}
private DownstreamType downstreamTypeToEnum(final int ifaceType) {
@@ -374,4 +611,64 @@
return UpstreamType.UT_UNKNOWN;
}
+
+ /**
+ * Check whether tethering metrics' data usage can be collected for a given upstream type.
+ *
+ * @param type the upstream type
+ */
+ public static boolean isUsageSupportedForUpstreamType(@NonNull UpstreamType type) {
+ switch(type) {
+ case UT_CELLULAR:
+ case UT_WIFI:
+ case UT_BLUETOOTH:
+ case UT_ETHERNET:
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Build NetworkTemplate for the given upstream type.
+ *
+ * <p> NetworkTemplate.Builder API was introduced in Android T.
+ *
+ * @param type the upstream type
+ * @return A NetworkTemplate object with a corresponding match rule or null if tethering
+ * metrics' data usage cannot be collected for a given upstream type.
+ */
+ @Nullable
+ public static NetworkTemplate buildNetworkTemplateForUpstreamType(@NonNull UpstreamType type) {
+ if (!isUsageSupportedForUpstreamType(type)) return null;
+
+ switch (type) {
+ case UT_CELLULAR:
+ // TODO: Handle the DUN connection, which is not a default network.
+ return new NetworkTemplate.Builder(MATCH_MOBILE)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ case UT_WIFI:
+ return new NetworkTemplate.Builder(MATCH_WIFI)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ case UT_BLUETOOTH:
+ return new NetworkTemplate.Builder(MATCH_BLUETOOTH)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ case UT_ETHERNET:
+ return new NetworkTemplate.Builder(MATCH_ETHERNET)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ default:
+ Log.e(TAG, "Unsupported UpstreamType: " + type.name());
+ break;
+ }
+ return null;
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
index 078a35f..c236188 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -22,7 +22,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
import java.util.List;
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index e6236df..76c2f0d 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -16,7 +16,6 @@
package com.android.networkstack.tethering.util;
import android.net.TetherStatsParcel;
-import android.net.TetheringRequestParcel;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -29,7 +28,6 @@
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
-import java.util.Objects;
/**
* The classes and the methods for tethering utilization.
@@ -158,20 +156,6 @@
return s & 0xffff;
}
- /** Check whether two TetheringRequestParcels are the same. */
- public static boolean isTetheringRequestEquals(final TetheringRequestParcel request,
- final TetheringRequestParcel otherRequest) {
- if (request == otherRequest) return true;
-
- return request != null && otherRequest != null
- && request.tetheringType == otherRequest.tetheringType
- && Objects.equals(request.localIPv4Address, otherRequest.localIPv4Address)
- && Objects.equals(request.staticClientAddress, otherRequest.staticClientAddress)
- && request.exemptFromEntitlementCheck == otherRequest.exemptFromEntitlementCheck
- && request.showProvisioningUi == otherRequest.showProvisioningUi
- && request.connectivityScope == otherRequest.connectivityScope;
- }
-
/** Get inet6 address for all nodes given scope ID. */
public static Inet6Address getAllNodesForScopeId(int scopeId) {
try {
diff --git a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
index c9e75c0..5eb1551 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
@@ -62,8 +62,8 @@
if (DBG) Log.d(mTag, "startListening");
if (mReceiver != null) return;
- mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
- mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+ mReceiver = new Receiver(mTag, mGenerationNumber, mCallback, mHandler);
+ mContext.registerReceiver(mReceiver, mFilter);
}
/** Stop listening to intent broadcast. */
@@ -77,30 +77,35 @@
}
private static class Receiver extends BroadcastReceiver {
- public final String tag;
- public final AtomicInteger atomicGenerationNumber;
- public final Consumer<Intent> callback;
+ final String mTag;
+ final AtomicInteger mAtomicGenerationNumber;
+ final Consumer<Intent> mCallback;
// Used to verify this receiver is still current.
- public final int generationNumber;
+ final int mGenerationNumber;
+ private final Handler mHandler;
- Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
- this.tag = tag;
- this.atomicGenerationNumber = atomicGenerationNumber;
- this.callback = callback;
- generationNumber = atomicGenerationNumber.incrementAndGet();
+ Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback,
+ Handler handler) {
+ mTag = tag;
+ mAtomicGenerationNumber = atomicGenerationNumber;
+ mCallback = callback;
+ mGenerationNumber = atomicGenerationNumber.incrementAndGet();
+ mHandler = handler;
}
@Override
public void onReceive(Context context, Intent intent) {
- final int currentGenerationNumber = atomicGenerationNumber.get();
+ mHandler.post(() -> {
+ final int currentGenerationNumber = mAtomicGenerationNumber.get();
- if (DBG) {
- Log.d(tag, "receiver generationNumber=" + generationNumber
- + ", current generationNumber=" + currentGenerationNumber);
- }
- if (generationNumber != currentGenerationNumber) return;
+ if (DBG) {
+ Log.d(mTag, "receiver generationNumber=" + mGenerationNumber
+ + ", current generationNumber=" + currentGenerationNumber);
+ }
+ if (mGenerationNumber != currentGenerationNumber) return;
- callback.accept(intent);
+ mCallback.accept(intent);
+ });
}
}
}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 337d408..2211546 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -38,9 +38,9 @@
"connectivity-net-module-utils-bpf",
],
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
],
}
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 120b871..423b9b8 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -16,7 +16,6 @@
package android.net;
-import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.TETHER_PRIVILEGED;
@@ -27,7 +26,9 @@
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringTester.buildTcpPacket;
import static android.net.TetheringTester.buildUdpPacket;
+import static android.net.TetheringTester.buildUdpPackets;
import static android.net.TetheringTester.isAddressIpv4;
+import static android.net.TetheringTester.isExpectedFragmentIpPacket;
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
@@ -42,7 +43,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
@@ -60,13 +60,14 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
+import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.FragmentHeader;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.testutils.HandlerUtils;
import com.android.testutils.TapPacketReader;
@@ -105,7 +106,7 @@
// Used to check if any tethering interface is available. Choose 200ms to be request timeout
// because the average interface requested time on cuttlefish@acloud is around 10ms.
// See TetheredInterfaceRequester.getInterface, isInterfaceForTetheringAvailable.
- private static final int AVAILABLE_TETHER_IFACE_REQUEST_TIMEOUT_MS = 200;
+ private static final int SHORT_TIMEOUT_MS = 1000;
private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
protected static final long WAIT_RA_TIMEOUT_MS = 2000;
@@ -154,7 +155,7 @@
private boolean mRunTests;
private HandlerThread mHandlerThread;
private Handler mHandler;
- private TetheredInterfaceRequester mTetheredInterfaceRequester;
+ protected TetheredInterfaceRequester mTetheredInterfaceRequester;
// Late initialization in initTetheringTester().
private TapPacketReader mUpstreamReader;
@@ -245,12 +246,14 @@
maybeUnregisterTetheringEventCallback(mTetheringEventCallback);
mTetheringEventCallback = null;
- runAsShell(NETWORK_SETTINGS, () -> mTetheredInterfaceRequester.release());
setIncludeTestInterfaces(false);
}
@After
public void tearDown() throws Exception {
+ if (mTetheredInterfaceRequester != null) {
+ mTetheredInterfaceRequester.release();
+ }
try {
if (mRunTests) cleanUp();
} finally {
@@ -263,33 +266,17 @@
}
}
- 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, () -> sEm.isAvailable());
- }
-
+ protected boolean isInterfaceForTetheringAvailable() throws Exception {
// 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();
- 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
- // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute
- // test module timeout on internal testing.
- // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving
- // this check into #setUpOnce.
- return requester.getInterface(AVAILABLE_TETHER_IFACE_REQUEST_TIMEOUT_MS) != null;
- } catch (TimeoutException e) {
- return false;
- } finally {
- runAsShell(NETWORK_SETTINGS, () -> {
- requester.release();
- });
- }
+ // Use short timeout (200ms) for requesting an existing interface, if any, because
+ // it should reurn faster than requesting a new tethering interface. Using default
+ // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute
+ // test module timeout on internal testing.
+ // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving
+ // this check into #setUpOnce.
+ return mTetheredInterfaceRequester.isPhysicalInterfaceAvailable(SHORT_TIMEOUT_MS);
}
protected static void setIncludeTestInterfaces(boolean include) {
@@ -304,14 +291,6 @@
});
}
- protected String getTetheredInterface() throws Exception {
- return mTetheredInterfaceRequester.getInterface();
- }
-
- protected CompletableFuture<String> requestTetheredInterface() throws Exception {
- return mTetheredInterfaceRequester.requestInterface();
- }
-
protected static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
long timeoutMs) {
final long deadline = SystemClock.uptimeMillis() + timeoutMs;
@@ -589,6 +568,12 @@
return nif.getMTU();
}
+ protected int getIndexByName(String ifaceName) throws SocketException {
+ NetworkInterface nif = NetworkInterface.getByName(ifaceName);
+ assertNotNull("Can't get NetworkInterface object for " + ifaceName, nif);
+ return nif.getIndex();
+ }
+
protected TapPacketReader makePacketReader(final TestNetworkInterface iface) throws Exception {
FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor();
return makePacketReader(fd, getMTU(iface));
@@ -605,6 +590,11 @@
private TetheredInterfaceRequest mRequest;
private final CompletableFuture<String> mFuture = new CompletableFuture<>();
+ TetheredInterfaceRequester() {
+ mRequest = runAsShell(NETWORK_SETTINGS, () ->
+ sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
+ }
+
@Override
public void onAvailable(String iface) {
Log.d(TAG, "Ethernet interface available: " + iface);
@@ -616,28 +606,21 @@
mFuture.completeExceptionally(new IllegalStateException("onUnavailable received"));
}
- public CompletableFuture<String> requestInterface() {
- assertNull("BUG: more than one tethered interface request", mRequest);
- Log.d(TAG, "Requesting tethered interface");
- mRequest = runAsShell(NETWORK_SETTINGS, () ->
- sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
- return mFuture;
- }
-
- public String getInterface(int timeout) throws Exception {
- return requestInterface().get(timeout, TimeUnit.MILLISECONDS);
+ public boolean isPhysicalInterfaceAvailable(int timeout) {
+ try {
+ final String iface = mFuture.get(timeout, TimeUnit.MILLISECONDS);
+ return !iface.startsWith("testtap");
+ } catch (Exception e) {
+ return false;
+ }
}
public String getInterface() throws Exception {
- return getInterface(TIMEOUT_MS);
+ return mFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
public void release() {
- if (mRequest != null) {
- mFuture.obtrudeException(new IllegalStateException("Request already released"));
- mRequest.release();
- mRequest = null;
- }
+ runAsShell(NETWORK_SETTINGS, () -> mRequest.release());
}
}
@@ -658,7 +641,10 @@
lp.setLinkAddresses(addresses);
lp.setDnsServers(dnses);
- return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
+ // TODO: initTestNetwork can take up to 15 seconds on a workstation. Investigate when and
+ // why this is the case. It is unclear whether a 30 second timeout is enough when running
+ // these tests in the much slower test infra.
+ return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, 30_000));
}
protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -702,6 +688,57 @@
});
}
+ protected void sendDownloadFragmentedUdpPackets(@NonNull final Inet6Address srcIp,
+ @NonNull final Inet6Address dstIp, @NonNull final TetheringTester tester,
+ @NonNull final ByteBuffer payload, int l2mtu) throws Exception {
+ final List<ByteBuffer> testPackets = buildUdpPackets(null /* srcMac */, null /* dstMac */,
+ srcIp, dstIp, REMOTE_PORT, LOCAL_PORT, payload, l2mtu);
+ assertTrue("No packet fragmentation occurs", testPackets.size() > 1);
+
+ short id = 0;
+ final ArrayMap<Short, ByteBuffer> fragmentPayloads = new ArrayMap<>();
+ for (ByteBuffer testPacket : testPackets) {
+ Struct.parse(Ipv6Header.class, testPacket);
+ final FragmentHeader fragmentHeader = Struct.parse(FragmentHeader.class, testPacket);
+ // Conversion of IPv6's fragmentOffset field to IPv4's flagsAndFragmentOffset field.
+ // IPv6 Fragment Header:
+ // '13 bits of offset in multiples of 8' + 2 zero bits + more fragment bit
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Next Header | Reserved | Fragment Offset |Res|M|
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Identification |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // IPv4 Header:
+ // zero bit + don't frag bit + more frag bit + '13 bits of offset in multiples of 8'
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // |Version| IHL |Type of Service| Total Length |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Identification |Flags| Fragment Offset |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // + . . . +
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ short offset = (short) (((fragmentHeader.fragmentOffset & 0x1) << 13)
+ | (fragmentHeader.fragmentOffset >> 3));
+ // RFC6145: for fragment id, copied from the low-order 16 bits in the identification
+ // field in the Fragment Header.
+ id = (short) (fragmentHeader.identification & 0xffff);
+ final byte[] fragmentPayload = new byte[testPacket.remaining()];
+ testPacket.get(fragmentPayload);
+ testPacket.flip();
+ fragmentPayloads.put(offset, ByteBuffer.wrap(fragmentPayload));
+ }
+
+ final short fragId = id;
+ tester.verifyDownloadBatch(testPackets, p -> {
+ Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+ return isExpectedFragmentIpPacket(p, fragId, fragmentPayloads);
+ });
+ }
+
protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp,
@NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags,
@NonNull final ByteBuffer payload, @NonNull final TetheringTester tester,
@@ -937,6 +974,11 @@
return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
}
+ protected String getUpstreamInterfaceName() {
+ if (mUpstreamTracker == null) return null;
+ return mUpstreamTracker.getTestIface().getInterfaceName();
+ }
+
protected <T> List<T> toList(T... array) {
return Arrays.asList(array);
}
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae4ae55..b152b4c 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -27,6 +27,7 @@
import static android.system.OsConstants.IPPROTO_IPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+
import static com.android.net.module.util.DnsPacket.ANSECTION;
import static com.android.net.module.util.DnsPacket.DnsHeader;
import static com.android.net.module.util.DnsPacket.DnsRecord;
@@ -53,6 +54,7 @@
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -91,6 +93,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
@@ -497,6 +500,51 @@
});
}
+ /**
+ * Checks if the given raw packet data represents an expected fragmented IP packet.
+ *
+ * @param rawPacket the raw packet data to check.
+ * @param id the identification field of the fragmented IP packet.
+ * @param expectedPayloads a map of fragment offsets to their corresponding payload data.
+ * @return true if the packet is a valid fragmented IP packet with matching payload fragments;
+ * false otherwise.
+ */
+ public static boolean isExpectedFragmentIpPacket(@NonNull final byte[] rawPacket, int id,
+ @NonNull final Map<Short, ByteBuffer> expectedPayloads) {
+ final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+ try {
+ // Validate Ethernet header and IPv4 header.
+ if (!hasExpectedEtherHeader(buf, true /* isIpv4 */)) return false;
+ final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
+ if (ipv4Header.protocol != (byte) IPPROTO_UDP) return false;
+ if (ipv4Header.id != id) return false;
+ // Validate payload data which expected at a specific fragment offset.
+ final ByteBuffer expectedPayload =
+ expectedPayloads.get(ipv4Header.flagsAndFragmentOffset);
+ if (expectedPayload == null) return false;
+ if (buf.remaining() != expectedPayload.limit()) return false;
+ // Validate UDP header (which located in the 1st fragment).
+ // TODO: Validate the checksum field in UDP header. Currently, it'll be altered by NAT.
+ if ((ipv4Header.flagsAndFragmentOffset & 0x1FFF) == 0) {
+ final UdpHeader receivedUdpHeader = Struct.parse(UdpHeader.class, buf);
+ final UdpHeader expectedUdpHeader = Struct.parse(UdpHeader.class, expectedPayload);
+ if (receivedUdpHeader == null || expectedUdpHeader == null) return false;
+ if (receivedUdpHeader.srcPort != expectedUdpHeader.srcPort
+ || receivedUdpHeader.dstPort != expectedUdpHeader.dstPort
+ || receivedUdpHeader.length != expectedUdpHeader.length) {
+ return false;
+ }
+ return true;
+ }
+ // Check the contents of the remaining payload.
+ return Arrays.equals(getRemaining(buf),
+ getRemaining(expectedPayload.asReadOnlyBuffer()));
+ } catch (Exception e) {
+ // A failed packet parsing indicates that the packet is not a fragmented IPv4 packet.
+ return false;
+ }
+ }
+
// |expectedPayload| is copied as read-only because the caller may reuse it.
// See hasExpectedDnsMessage.
public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
@@ -683,10 +731,10 @@
}
@NonNull
- public static ByteBuffer buildUdpPacket(
+ public static List<ByteBuffer> buildUdpPackets(
@Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
- short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+ short srcPort, short dstPort, @Nullable final ByteBuffer payload, int l2mtu)
throws Exception {
final int ipProto = getIpProto(srcIp, dstIp);
final boolean hasEther = (srcMac != null && dstMac != null);
@@ -720,7 +768,30 @@
payload.clear();
}
- return packetBuilder.finalizePacket();
+ return l2mtu == 0
+ ? Arrays.asList(packetBuilder.finalizePacket())
+ : packetBuilder.finalizePacket(l2mtu);
+ }
+
+ /**
+ * Builds a UDP packet.
+ *
+ * @param srcMac the source MAC address.
+ * @param dstMac the destination MAC address.
+ * @param srcIp the source IP address.
+ * @param dstIp the destination IP address.
+ * @param srcPort the source port number.
+ * @param dstPort the destination port number.
+ * @param payload the optional payload data to be included in the packet.
+ * @return a ByteBuffer containing the constructed UDP packet.
+ */
+ @NonNull
+ public static ByteBuffer buildUdpPacket(
+ @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+ @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+ short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+ throws Exception {
+ return buildUdpPackets(srcMac, dstMac, srcIp, dstIp, srcPort, dstPort, payload, 0).get(0);
}
@NonNull
@@ -994,6 +1065,28 @@
return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
}
+ /**
+ * Sends a batch of download packets and verifies against a specified filtering condition.
+ *
+ * This method is designed for testing fragmented packets. All packets are sent before
+ * verification because the kernel buffers fragments until the last one is received.
+ * Captured packets are then verified against the provided filter.
+ *
+ * @param packets the list of ByteBuffers containing the packets to send.
+ * @param filter a Predicate that defines the filtering condition to apply to each received
+ * packet. If the filter returns true for a packet's data, it is considered to
+ * meet the verification criteria.
+ */
+ public void verifyDownloadBatch(final List<ByteBuffer> packets, final Predicate<byte[]> filter)
+ throws Exception {
+ for (ByteBuffer packet : packets) {
+ sendDownloadPacket(packet);
+ }
+ for (int i = 0; i < packets.size(); ++i) {
+ verifyPacketNotNull("Download fail", getDownloadPacket(filter));
+ }
+ }
+
// Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
// the upcoming DHCP packets. This method should be only used when we know the DHCP
// server has been created successfully before.
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index c54d1b4..32b2f3e 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -16,6 +16,7 @@
package android.net;
+import static android.Manifest.permission.DUMP;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
@@ -26,40 +27,62 @@
import static android.net.TetheringTester.isExpectedUdpDnsPacket;
import static android.system.OsConstants.ICMP_ECHO;
import static android.system.OsConstants.ICMP_ECHOREPLY;
+import static android.system.OsConstants.IPPROTO_UDP;
import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
import static com.android.net.module.util.HexDump.dumpHexString;
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.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
+import static com.android.testutils.DeviceInfoUtils.KVersion;
+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;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
+import android.content.Context;
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice;
import android.os.Build;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.BpfDump;
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
+import com.android.net.module.util.bpf.Tether4Key;
+import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.structs.Ipv4Header;
import com.android.net.module.util.structs.UdpHeader;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
+import com.android.testutils.DumpTestUtils;
import com.android.testutils.NetworkStackModuleTest;
import com.android.testutils.TapPacketReader;
-import org.junit.BeforeClass;
+import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,10 +96,10 @@
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Random;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@RunWith(AndroidJUnit4.class)
@@ -91,6 +114,28 @@
private static final short ICMPECHO_ID = 0x0;
private static final short ICMPECHO_SEQ = 0x0;
+ private static final int DUMP_POLLING_MAX_RETRY = 100;
+ private static final int DUMP_POLLING_INTERVAL_MS = 50;
+ // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
+ // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
+ private static final int UDP_STREAM_TS_MS = 2000;
+ // Give slack time for waiting UDP stream mode because handling conntrack event in user space
+ // may not in precise time. Used to reduce the flaky rate.
+ private static final int UDP_STREAM_SLACK_MS = 500;
+ // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+ private static final int RX_UDP_PACKET_SIZE = 30;
+ private static final int RX_UDP_PACKET_COUNT = 456;
+ // Per TX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+ private static final int TX_UDP_PACKET_SIZE = 30;
+ private static final int TX_UDP_PACKET_COUNT = 123;
+
+ private static final String DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG = "clatEgress4RawBpfMap";
+ private static final String DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG = "clatIngress6RawBpfMap";
+ private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
+ private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+ private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
+ private static final String LINE_DELIMITER = "\\n";
+
// TODO: use class DnsPacket to build DNS query and reply message once DnsPacket supports
// building packet for given arguments.
private static final ByteBuffer DNS_QUERY = ByteBuffer.wrap(new byte[] {
@@ -151,33 +196,14 @@
(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04 /* Address: 1.2.3.4 */
};
- /** Enable/disable tethering once before running the tests. */
- @BeforeClass
- public static void setUpOnce() throws Exception {
- // The first test case may experience tethering restart with IP conflict handling.
- // 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.
- MyTetheringEventCallback callback = null;
- TestNetworkInterface testIface = null;
- assumeTrue(sEm != null);
- try {
- // If the physical ethernet interface is available, do nothing.
- if (isInterfaceForTetheringAvailable()) return;
-
- testIface = createTestInterface();
- setIncludeTestInterfaces(true);
-
- callback = enableEthernetTethering(testIface.getInterfaceName(), null);
- callback.awaitUpstreamChanged(true /* throwTimeoutException */);
- } catch (TimeoutException e) {
- Log.d(TAG, "WARNNING " + e);
- } finally {
- maybeCloseTestInterface(testIface);
- maybeUnregisterTetheringEventCallback(callback);
-
- setIncludeTestInterfaces(false);
- }
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ // TODO: See b/318121782#comment4. Register an ethernet InterfaceStateListener, and wait for
+ // the callback to report client mode. This happens as soon as both
+ // TetheredInterfaceRequester and the tethering code itself have released the interface,
+ // i.e. after stopTethering() has completed.
+ Thread.sleep(3000);
}
@Test
@@ -201,7 +227,7 @@
Log.d(TAG, "Including test interfaces");
setIncludeTestInterfaces(true);
- final String iface = getTetheredInterface();
+ final String iface = mTetheredInterfaceRequester.getInterface();
assertEquals("TetheredInterfaceCallback for unexpected interface",
downstreamIface.getInterfaceName(), iface);
@@ -223,8 +249,6 @@
// This test requires manipulating packets. Skip if there is a physical Ethernet connected.
assumeFalse(isInterfaceForTetheringAvailable());
- CompletableFuture<String> futureIface = requestTetheredInterface();
-
setIncludeTestInterfaces(true);
TestNetworkInterface downstreamIface = null;
@@ -234,7 +258,7 @@
try {
downstreamIface = createTestInterface();
- final String iface = futureIface.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ final String iface = mTetheredInterfaceRequester.getInterface();
assertEquals("TetheredInterfaceCallback for unexpected interface",
downstreamIface.getInterfaceName(), iface);
@@ -264,7 +288,7 @@
try {
downstreamIface = createTestInterface();
- final String iface = getTetheredInterface();
+ final String iface = mTetheredInterfaceRequester.getInterface();
assertEquals("TetheredInterfaceCallback for unexpected interface",
downstreamIface.getInterfaceName(), iface);
@@ -338,7 +362,7 @@
try {
downstreamIface = createTestInterface();
- final String iface = getTetheredInterface();
+ final String iface = mTetheredInterfaceRequester.getInterface();
assertEquals("TetheredInterfaceCallback for unexpected interface",
downstreamIface.getInterfaceName(), iface);
@@ -388,7 +412,7 @@
MyTetheringEventCallback tetheringEventCallback = null;
try {
// Get an interface to use.
- final String iface = getTetheredInterface();
+ final String iface = mTetheredInterfaceRequester.getInterface();
// Enable Ethernet tethering and check that it starts.
tetheringEventCallback = enableEthernetTethering(iface, null /* any upstream */);
@@ -509,17 +533,23 @@
// TODO: test BPF offload maps {rule, stats}.
}
- // Test network topology:
- //
- // public network (rawip) private network
- // | UE |
- // +------------+ V +------------+------------+ V +------------+
- // | Sever +---------+ Upstream | Downstream +---------+ Client |
- // +------------+ +------------+------------+ +------------+
- // remote ip public ip private ip
- // 8.8.8.8:443 <Upstream ip>:9876 <TetheredDevice ip>:9876
- //
- private void runUdp4Test() throws Exception {
+
+ /**
+ * Basic IPv4 UDP tethering test. Verify that UDP tethered packets are transferred no matter
+ * using which data path.
+ */
+ @Test
+ public void testTetherUdpV4() throws Exception {
+ // Test network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ // remote ip public ip private ip
+ // 8.8.8.8:443 <Upstream ip>:9876 <TetheredDevice ip>:9876
+ //
final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
toList(TEST_IP4_DNS));
final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
@@ -541,15 +571,6 @@
sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
}
- /**
- * Basic IPv4 UDP tethering test. Verify that UDP tethered packets are transferred no matter
- * using which data path.
- */
- @Test
- public void testTetherUdpV4() throws Exception {
- runUdp4Test();
- }
-
// Test network topology:
//
// public network (rawip) private network
@@ -599,7 +620,7 @@
final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
// TODO: remove the connectivity verification for upstream connected notification race.
- // See the same reason in runUdp4Test().
+ // See the same reason in testTetherUdp4().
probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
final ByteBuffer request = buildIcmpEchoPacketV4(tethered.macAddr /* srcMac */,
@@ -707,7 +728,7 @@
final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
// TODO: remove the connectivity verification for upstream connected notification race.
- // See the same reason in runUdp4Test().
+ // See the same reason in testTetherUdp4().
probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
// [1] Send DNS query.
@@ -751,7 +772,7 @@
final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
// TODO: remove the connectivity verification for upstream connected notification race.
- // See the same reason in runUdp4Test().
+ // See the same reason in testTetherUdp4().
probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
runTcpTest(tethered.macAddr /* uploadSrcMac */, tethered.routerMacAddr /* uploadDstMac */,
@@ -828,4 +849,335 @@
final MacAddress macAddress = MacAddress.fromString("11:22:33:44:55:66");
assertTrue(tester.testDhcpServerAlive(macAddress));
}
+
+ private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
+ final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+ return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
+ || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
+ || current.isAtLeast(new KVersion(5, 4, 98));
+ }
+
+ @Test
+ public void testIsUdpOffloadSupportedByKernel() throws Exception {
+ assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
+ assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
+
+ assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
+ assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
+
+ assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
+ }
+
+ private static void assumeKernelSupportBpfOffloadUdpV4() {
+ final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+ assumeTrue("Kernel version " + kernelVersion + " doesn't support IPv4 UDP BPF offload",
+ isUdpOffloadSupportedByKernel(kernelVersion));
+ }
+
+ @Test
+ public void testKernelSupportBpfOffloadUdpV4() throws Exception {
+ assumeKernelSupportBpfOffloadUdpV4();
+ }
+
+ private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+ final String dumpStr = runAsShell(DUMP, () ->
+ DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short"));
+
+ // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+ // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+ // RRO to override the enabled default value. Get the tethering config via dumpsys.
+ // $ dumpsys tethering
+ // mIsBpfEnabled: true
+ boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+ if (!enabled) {
+ Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+ }
+ return enabled;
+ }
+
+ @Test
+ public void testTetherConfigBpfOffloadEnabled() throws Exception {
+ assumeTrue(isTetherConfigBpfOffloadEnabled());
+ }
+
+ @NonNull
+ private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+ Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
+ throws Exception {
+ final String rawMapStr = runAsShell(DUMP, () ->
+ DumpTestUtils.dumpService(service, args));
+ final HashMap<K, V> map = new HashMap<>();
+
+ for (final String line : rawMapStr.split(LINE_DELIMITER)) {
+ final Pair<K, V> rule =
+ BpfDump.fromBase64EncodedString(keyClass, valueClass, line.trim());
+ map.put(rule.first, rule.second);
+ }
+ return map;
+ }
+
+ @Nullable
+ private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+ Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
+ throws Exception {
+ for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
+ final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, service, args);
+ if (!map.isEmpty()) return map;
+
+ Thread.sleep(DUMP_POLLING_INTERVAL_MS);
+ }
+
+ fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
+ return null;
+ }
+
+ // Test network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ // remote ip public ip private ip
+ // 8.8.8.8:443 <Upstream ip>:9876 <TetheredDevice ip>:9876
+ //
+ private void runUdp4Test() throws Exception {
+ final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+ toList(TEST_IP4_DNS));
+ final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+ // TODO: remove the connectivity verification for upstream connected notification race.
+ // Because async upstream connected notification can't guarantee the tethering routing is
+ // ready to use. Need to test tethering connectivity before testing.
+ // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
+ // from upstream. That can guarantee that the routing is ready. Long term plan is that
+ // refactors upstream connected notification from async to sync.
+ probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
+
+ final MacAddress srcMac = tethered.macAddr;
+ final MacAddress dstMac = tethered.routerMacAddr;
+ final InetAddress remoteIp = REMOTE_IP4_ADDR;
+ final InetAddress tetheringUpstreamIp = TEST_IP4_ADDR.getAddress();
+ final InetAddress clientIp = tethered.ipv4Addr;
+ sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
+ sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
+
+ // Send second UDP packet in original direction.
+ // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
+ // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
+ // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
+ // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
+ // and apply ASSURED flag.
+ // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
+ // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
+ Thread.sleep(UDP_STREAM_TS_MS);
+ sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
+
+ // Give a slack time for handling conntrack event in user space.
+ Thread.sleep(UDP_STREAM_SLACK_MS);
+
+ // [1] Verify IPv4 upstream rule map.
+ final String[] upstreamArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+ DUMPSYS_RAWMAP_ARG_UPSTREAM4};
+ final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+ Tether4Key.class, Tether4Value.class, Context.TETHERING_SERVICE, upstreamArgs);
+ assertNotNull(upstreamMap);
+ assertEquals(1, upstreamMap.size());
+
+ final Map.Entry<Tether4Key, Tether4Value> rule =
+ upstreamMap.entrySet().iterator().next();
+
+ final Tether4Key upstream4Key = rule.getKey();
+ assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+ assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+ assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+ assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+ assertEquals(REMOTE_PORT, upstream4Key.dstPort);
+
+ final Tether4Value upstream4Value = rule.getValue();
+ assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(),
+ InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+ assertEquals(LOCAL_PORT, upstream4Value.srcPort);
+ assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
+ InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+ assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+ // [2] Verify stats map.
+ // Transmit packets on both direction for verifying stats. Because we only care the
+ // packet count in stats test, we just reuse the existing packets to increaes
+ // the packet count on both direction.
+
+ // Send packets on original direction.
+ for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+ sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester,
+ false /* is4To6 */);
+ }
+
+ // Send packets on reply direction.
+ for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+ sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
+ }
+
+ // Dump stats map to verify.
+ final String[] statsArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+ DUMPSYS_RAWMAP_ARG_STATS};
+ final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+ TetherStatsKey.class, TetherStatsValue.class, Context.TETHERING_SERVICE, statsArgs);
+ assertNotNull(statsMap);
+ assertEquals(1, statsMap.size());
+
+ final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+ statsMap.entrySet().iterator().next();
+
+ // TODO: verify the upstream index in TetherStatsKey.
+
+ final TetherStatsValue statsValue = stats.getValue();
+ assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+ assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+ assertEquals(0, statsValue.rxErrors);
+ assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+ assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+ assertEquals(0, statsValue.txErrors);
+ }
+
+ /**
+ * BPF offload IPv4 UDP tethering test. Verify that UDP tethered packets are offloaded by BPF.
+ * Minimum test requirement:
+ * 1. S+ device.
+ * 2. Tethering config enables tethering BPF offload.
+ * 3. Kernel supports IPv4 UDP BPF offload. See #isUdpOffloadSupportedByKernel.
+ *
+ * TODO: consider enabling the test even tethering config disables BPF offload. See b/238288883
+ */
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testTetherBpfOffloadUdpV4() throws Exception {
+ assumeTrue("Tethering config disabled BPF offload", isTetherConfigBpfOffloadEnabled());
+ assumeKernelSupportBpfOffloadUdpV4();
+
+ runUdp4Test();
+ }
+
+ private ClatEgress4Value getClatEgress4Value(int clatIfaceIndex) throws Exception {
+ // Command: dumpsys connectivity clatEgress4RawBpfMap
+ final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG};
+ final HashMap<ClatEgress4Key, ClatEgress4Value> egress4Map = pollRawMapFromDump(
+ ClatEgress4Key.class, ClatEgress4Value.class, Context.CONNECTIVITY_SERVICE, args);
+ assertNotNull(egress4Map);
+ for (Map.Entry<ClatEgress4Key, ClatEgress4Value> entry : egress4Map.entrySet()) {
+ ClatEgress4Key key = entry.getKey();
+ if (key.iif == clatIfaceIndex) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ private ClatIngress6Value getClatIngress6Value(int ifaceIndex) throws Exception {
+ // Command: dumpsys connectivity clatIngress6RawBpfMap
+ final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG};
+ final HashMap<ClatIngress6Key, ClatIngress6Value> ingress6Map = pollRawMapFromDump(
+ ClatIngress6Key.class, ClatIngress6Value.class, Context.CONNECTIVITY_SERVICE, args);
+ assertNotNull(ingress6Map);
+ for (Map.Entry<ClatIngress6Key, ClatIngress6Value> entry : ingress6Map.entrySet()) {
+ ClatIngress6Key key = entry.getKey();
+ if (key.iif == ifaceIndex) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Test network topology:
+ *
+ * public network (rawip) private network
+ * | UE (CLAT support) |
+ * +---------------+ V +------------+------------+ V +------------+
+ * | NAT64 Gateway +---------+ Upstream | Downstream +---------+ Client |
+ * +---------------+ +------------+------------+ +------------+
+ * remote ip public ip private ip
+ * [64:ff9b::808:808]:443 [clat ipv6]:9876 [TetheredDevice ipv4]:9876
+ *
+ * Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+ * sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+ * packet.
+ */
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testTetherClatBpfOffloadUdp() throws Exception {
+ assumeKernelSupportBpfOffloadUdpV4();
+
+ // CLAT only starts on IPv6 only network.
+ final TetheringTester tester = initTetheringTester(toList(TEST_IP6_ADDR),
+ toList(TEST_IP6_DNS));
+ final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, true /* hasIpv6 */);
+
+ // Get CLAT IPv6 address.
+ final Inet6Address clatIp6 = getClatIpv6Address(tester, tethered);
+
+ // Get current values before sending packets.
+ final String ifaceName = getUpstreamInterfaceName();
+ final int ifaceIndex = getIndexByName(ifaceName);
+ final int clatIfaceIndex = getIndexByName("v4-" + ifaceName);
+ final ClatEgress4Value oldEgress4 = getClatEgress4Value(clatIfaceIndex);
+ final ClatIngress6Value oldIngress6 = getClatIngress6Value(ifaceIndex);
+ assertNotNull(oldEgress4);
+ assertNotNull(oldIngress6);
+
+ // Send an IPv4 UDP packet in original direction.
+ // IPv4 packet -- CLAT translation --> IPv6 packet
+ for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+ sendUploadPacketUdp(tethered.macAddr, tethered.routerMacAddr, tethered.ipv4Addr,
+ REMOTE_IP4_ADDR, tester, true /* is4To6 */);
+ }
+
+ // Send an IPv6 UDP packet in reply direction.
+ // IPv6 packet -- CLAT translation --> IPv4 packet
+ for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+ sendDownloadPacketUdp(REMOTE_NAT64_ADDR, clatIp6, tester, true /* is6To4 */);
+ }
+
+ // Send fragmented IPv6 UDP packets in the reply direction.
+ // IPv6 frament packet -- CLAT translation --> IPv4 fragment packet
+ final int payloadLen = 1500;
+ final int l2mtu = 1000;
+ final int fragPktCnt = 2; // 1500 bytes of UDP payload were fragmented into two packets.
+ final long fragRxBytes = payloadLen + UDP_HEADER_LEN + fragPktCnt * IPV4_HEADER_MIN_LEN;
+ final byte[] payload = new byte[payloadLen];
+ // Initialize the payload with random bytes.
+ Random random = new Random();
+ random.nextBytes(payload);
+ sendDownloadFragmentedUdpPackets(REMOTE_NAT64_ADDR, clatIp6, tester,
+ ByteBuffer.wrap(payload), l2mtu);
+
+ // After sending test packets, get stats again to verify their differences.
+ final ClatEgress4Value newEgress4 = getClatEgress4Value(clatIfaceIndex);
+ final ClatIngress6Value newIngress6 = getClatIngress6Value(ifaceIndex);
+ assertNotNull(newEgress4);
+ assertNotNull(newIngress6);
+
+ assertEquals(RX_UDP_PACKET_COUNT + fragPktCnt, newIngress6.packets - oldIngress6.packets);
+ assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE + fragRxBytes,
+ newIngress6.bytes - oldIngress6.bytes);
+ assertEquals(TX_UDP_PACKET_COUNT, newEgress4.packets - oldEgress4.packets);
+ // The increase in egress traffic equals the expected size of the translated UDP packets.
+ // Calculation:
+ // - Original UDP packet was TX_UDP_PACKET_SIZE bytes (IPv4 header + UDP header + payload).
+ // - After CLAT translation, each packet is now:
+ // IPv6 header + unchanged UDP header + unchanged payload
+ // Therefore, the total size of the translated UDP packet should be:
+ // TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN
+ assertEquals(
+ TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
+ newEgress4.bytes - oldEgress4.bytes);
+ }
}
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index c4d5636..1f1929c 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -26,7 +26,7 @@
target_sdk_version: "33",
libs: [
- "android.test.base",
+ "android.test.base.stubs",
],
srcs: [
diff --git a/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java b/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java
deleted file mode 100644
index c2bc812..0000000
--- a/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java
+++ /dev/null
@@ -1,304 +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 android.net;
-
-import static android.Manifest.permission.DUMP;
-import static android.system.OsConstants.IPPROTO_UDP;
-
-import static com.android.testutils.DeviceInfoUtils.KVersion;
-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;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.net.TetheringTester.TetheredDevice;
-import android.os.Build;
-import android.os.VintfRuntimeInfo;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.net.module.util.BpfDump;
-import com.android.net.module.util.Struct;
-import com.android.net.module.util.bpf.Tether4Key;
-import com.android.net.module.util.bpf.Tether4Value;
-import com.android.net.module.util.bpf.TetherStatsKey;
-import com.android.net.module.util.bpf.TetherStatsValue;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DeviceInfoUtils;
-import com.android.testutils.DumpTestUtils;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.net.InetAddress;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-public class MtsEthernetTetheringTest extends EthernetTetheringTestBase {
- @Rule
- public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
- private static final String TAG = MtsEthernetTetheringTest.class.getSimpleName();
-
- private static final int DUMP_POLLING_MAX_RETRY = 100;
- private static final int DUMP_POLLING_INTERVAL_MS = 50;
- // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
- // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
- private static final int UDP_STREAM_TS_MS = 2000;
- // Give slack time for waiting UDP stream mode because handling conntrack event in user space
- // may not in precise time. Used to reduce the flaky rate.
- private static final int UDP_STREAM_SLACK_MS = 500;
- // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
- private static final int RX_UDP_PACKET_SIZE = 30;
- private static final int RX_UDP_PACKET_COUNT = 456;
- // Per TX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
- private static final int TX_UDP_PACKET_SIZE = 30;
- private static final int TX_UDP_PACKET_COUNT = 123;
-
- private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
- private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
- private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
- private static final String LINE_DELIMITER = "\\n";
-
- private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
- final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
- return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
- || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
- || current.isAtLeast(new KVersion(5, 4, 98));
- }
-
- @Test
- public void testIsUdpOffloadSupportedByKernel() throws Exception {
- assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
- assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
- assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
- assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
- assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
-
- assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
- assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
- assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
- assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
- assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
-
- assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
- assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
- assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
- }
-
- private static void assumeKernelSupportBpfOffloadUdpV4() {
- final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
- assumeTrue("Kernel version " + kernelVersion + " doesn't support IPv4 UDP BPF offload",
- isUdpOffloadSupportedByKernel(kernelVersion));
- }
-
- @Test
- public void testKernelSupportBpfOffloadUdpV4() throws Exception {
- assumeKernelSupportBpfOffloadUdpV4();
- }
-
- private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
- final String dumpStr = runAsShell(DUMP, () ->
- DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short"));
-
- // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
- // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
- // RRO to override the enabled default value. Get the tethering config via dumpsys.
- // $ dumpsys tethering
- // mIsBpfEnabled: true
- boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
- if (!enabled) {
- Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
- }
- return enabled;
- }
-
- @Test
- public void testTetherConfigBpfOffloadEnabled() throws Exception {
- assumeTrue(isTetherConfigBpfOffloadEnabled());
- }
-
- @NonNull
- private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
- Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
- throws Exception {
- final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
- final String rawMapStr = runAsShell(DUMP, () ->
- DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
- final HashMap<K, V> map = new HashMap<>();
-
- for (final String line : rawMapStr.split(LINE_DELIMITER)) {
- final Pair<K, V> rule =
- BpfDump.fromBase64EncodedString(keyClass, valueClass, line.trim());
- map.put(rule.first, rule.second);
- }
- return map;
- }
-
- @Nullable
- private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
- Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
- throws Exception {
- for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
- final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
- if (!map.isEmpty()) return map;
-
- Thread.sleep(DUMP_POLLING_INTERVAL_MS);
- }
-
- fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
- return null;
- }
-
- // Test network topology:
- //
- // public network (rawip) private network
- // | UE |
- // +------------+ V +------------+------------+ V +------------+
- // | Sever +---------+ Upstream | Downstream +---------+ Client |
- // +------------+ +------------+------------+ +------------+
- // remote ip public ip private ip
- // 8.8.8.8:443 <Upstream ip>:9876 <TetheredDevice ip>:9876
- //
- private void runUdp4Test() throws Exception {
- final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
- toList(TEST_IP4_DNS));
- final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
-
- // TODO: remove the connectivity verification for upstream connected notification race.
- // Because async upstream connected notification can't guarantee the tethering routing is
- // ready to use. Need to test tethering connectivity before testing.
- // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
- // from upstream. That can guarantee that the routing is ready. Long term plan is that
- // refactors upstream connected notification from async to sync.
- probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
-
- final MacAddress srcMac = tethered.macAddr;
- final MacAddress dstMac = tethered.routerMacAddr;
- final InetAddress remoteIp = REMOTE_IP4_ADDR;
- final InetAddress tetheringUpstreamIp = TEST_IP4_ADDR.getAddress();
- final InetAddress clientIp = tethered.ipv4Addr;
- sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
- sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
-
- // Send second UDP packet in original direction.
- // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
- // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
- // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
- // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
- // and apply ASSURED flag.
- // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
- // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
- Thread.sleep(UDP_STREAM_TS_MS);
- sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
-
- // Give a slack time for handling conntrack event in user space.
- Thread.sleep(UDP_STREAM_SLACK_MS);
-
- // [1] Verify IPv4 upstream rule map.
- final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
- Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
- assertNotNull(upstreamMap);
- assertEquals(1, upstreamMap.size());
-
- final Map.Entry<Tether4Key, Tether4Value> rule =
- upstreamMap.entrySet().iterator().next();
-
- final Tether4Key upstream4Key = rule.getKey();
- assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
- assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
- assertEquals(LOCAL_PORT, upstream4Key.srcPort);
- assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
- assertEquals(REMOTE_PORT, upstream4Key.dstPort);
-
- final Tether4Value upstream4Value = rule.getValue();
- assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(),
- InetAddress.getByAddress(upstream4Value.src46).getAddress()));
- assertEquals(LOCAL_PORT, upstream4Value.srcPort);
- assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
- InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
- assertEquals(REMOTE_PORT, upstream4Value.dstPort);
-
- // [2] Verify stats map.
- // Transmit packets on both direction for verifying stats. Because we only care the
- // packet count in stats test, we just reuse the existing packets to increaes
- // the packet count on both direction.
-
- // Send packets on original direction.
- for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
- sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester,
- false /* is4To6 */);
- }
-
- // Send packets on reply direction.
- for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
- sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
- }
-
- // Dump stats map to verify.
- final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
- TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
- assertNotNull(statsMap);
- assertEquals(1, statsMap.size());
-
- final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
- statsMap.entrySet().iterator().next();
-
- // TODO: verify the upstream index in TetherStatsKey.
-
- final TetherStatsValue statsValue = stats.getValue();
- assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
- assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
- assertEquals(0, statsValue.rxErrors);
- assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
- assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
- assertEquals(0, statsValue.txErrors);
- }
-
- /**
- * BPF offload IPv4 UDP tethering test. Verify that UDP tethered packets are offloaded by BPF.
- * Minimum test requirement:
- * 1. S+ device.
- * 2. Tethering config enables tethering BPF offload.
- * 3. Kernel supports IPv4 UDP BPF offload. See #isUdpOffloadSupportedByKernel.
- *
- * TODO: consider enabling the test even tethering config disables BPF offload. See b/238288883
- */
- @Test
- @IgnoreUpTo(Build.VERSION_CODES.R)
- public void testTetherBpfOffloadUdpV4() throws Exception {
- assumeTrue("Tethering config disabled BPF offload", isTetherConfigBpfOffloadEnabled());
- assumeKernelSupportBpfOffloadUdpV4();
-
- runUdp4Test();
- }
-}
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index ba6be66..3597a91 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -53,4 +53,5 @@
"TetheringApiCurrentLib",
],
compile_multilib: "both",
+ min_sdk_version: "30",
}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 24407ca..d0d23ac 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -62,9 +62,9 @@
// remove framework-minus-apex, ext, and framework-res
sdk_version: "core_platform",
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
"ext",
"framework-minus-apex",
"framework-res",
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index a7064e8..177296a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -51,9 +51,9 @@
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
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;
@@ -74,7 +74,6 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
-import android.net.RoutingCoordinatorManager;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpEventCallbacks;
@@ -92,9 +91,9 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetheringConfiguration;
@@ -174,11 +173,9 @@
@Mock private IDhcpServer mDhcpServer;
@Mock private DadProxy mDadProxy;
@Mock private RouterAdvertisementDaemon mRaDaemon;
- @Mock private IpNeighborMonitor mIpNeighborMonitor;
@Mock private IpServer.Dependencies mDependencies;
@Mock private PrivateAddressCoordinator mAddressCoordinator;
- private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
- new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
+ @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
@Mock private TetheringMetrics mTetheringMetrics;
@@ -213,20 +210,17 @@
mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH;
}
- doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(), any());
-
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
mIpServer = createIpServer(interfaceType);
- verify(mIpNeighborMonitor).start();
mIpServer.start();
// Starting the state machine always puts us in a consistent state and notifies
// the rest of the world that we've changed from an unknown to available state.
mLooper.dispatchAll();
- reset(mNetd, mCallback, mIpNeighborMonitor);
+ reset(mNetd, mCallback);
when(mRaDaemon.start()).thenReturn(true);
}
@@ -242,6 +236,7 @@
throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ verify(mBpfCoordinator).addIpServer(mIpServer);
if (upstreamIface != null) {
InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
@@ -250,8 +245,12 @@
lp.setLinkAddresses(upstreamAddresses);
dispatchTetherConnectionChanged(upstreamIface, lp, 0);
Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, interfaceParams.index, upstreamPrefixes);
+ // One is called when handling CMD_TETHER_CONNECTION_CHANGED and the other one is called
+ // when upstream's LinkProperties is updated (updateUpstreamIPv6LinkProperties)
+ verify(mBpfCoordinator, times(2)).maybeAddUpstreamToLookupTable(
+ interfaceParams.index, upstreamIface);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, interfaceParams.index, upstreamPrefixes);
}
reset(mNetd, mBpfCoordinator, mCallback, mAddressCoordinator);
when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
@@ -281,24 +280,6 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
- // Simulate the behavior of RoutingCoordinator
- if (null != mRoutingCoordinatorManager.value) {
- doAnswer(it -> {
- final String fromIface = (String) it.getArguments()[0];
- final String toIface = (String) it.getArguments()[1];
- mNetd.tetherAddForward(fromIface, toIface);
- mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
- return null;
- }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
- doAnswer(it -> {
- final String fromIface = (String) it.getArguments()[0];
- final String toIface = (String) it.getArguments()[1];
- mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
- mNetd.tetherRemoveForward(fromIface, toIface);
- return null;
- }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
- }
-
setUpDhcpServer();
}
@@ -314,8 +295,6 @@
@Test
public void startsOutAvailable() throws Exception {
- when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
- .thenReturn(mIpNeighborMonitor);
mIpServer = createIpServer(TETHERING_BLUETOOTH);
mIpServer.start();
mLooper.dispatchAll();
@@ -458,107 +437,116 @@
// Telling the state machine about its upstream interface triggers
// a little more configuration.
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+ InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX,
UPSTREAM_IFACE);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
- verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+ verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
}
@Test
public void handlesChangingUpstream() throws Exception {
initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+ clearInvocations(mBpfCoordinator, mRoutingCoordinatorManager);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
- InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+ InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
// Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mRoutingCoordinatorManager)
+ .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
UPSTREAM_IFACE2);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
- verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+ verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
}
@Test
public void handlesChangingUpstreamNatFailure() throws Exception {
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
- doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+ doThrow(RuntimeException.class)
+ .when(mRoutingCoordinatorManager)
+ .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
- InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+ InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
// Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mRoutingCoordinatorManager)
+ .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
- // tetherAddForward.
+ // addInterfaceForward.
inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
UPSTREAM_IFACE2);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
// Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mRoutingCoordinatorManager)
+ .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
}
@Test
public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception {
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
- doThrow(RemoteException.class).when(mNetd).ipfwdAddInterfaceForward(
- IFACE_NAME, UPSTREAM_IFACE2);
+ doThrow(RuntimeException.class)
+ .when(mRoutingCoordinatorManager)
+ .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
- InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+ InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
// Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mRoutingCoordinatorManager)
+ .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
// ipfwdAddInterfaceForward.
inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
UPSTREAM_IFACE2);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
// Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
- inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mRoutingCoordinatorManager)
+ .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
}
@Test
public void canUnrequestTetheringWithUpstream() throws Exception {
initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+ clearInvocations(
+ mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
- InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+ InOrder inOrder =
+ inOrder(
+ mNetd,
+ mCallback,
+ mAddressCoordinator,
+ mBpfCoordinator,
+ mRoutingCoordinatorManager);
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ inOrder.verify(mRoutingCoordinatorManager)
+ .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, NO_UPSTREAM, NO_PREFIXES);
// When tethering stops, upstream interface is set to zero and thus clearing all upstream
// rules. Downstream rules are needed to be cleared explicitly by calling
// BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
@@ -570,12 +558,13 @@
argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
inOrder.verify(mAddressCoordinator).releaseDownstream(any());
inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
- inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
+ inOrder.verify(mBpfCoordinator).removeIpServer(mIpServer);
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
inOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), any(LinkProperties.class));
- verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+ verifyNoMoreInteractions(
+ mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
}
@Test
@@ -627,10 +616,14 @@
public void shouldTearDownUsbOnUpstreamError() throws Exception {
initTetheredStateMachine(TETHERING_USB, null);
- doThrow(RemoteException.class).when(mNetd).tetherAddForward(anyString(), anyString());
+ doThrow(RuntimeException.class)
+ .when(mRoutingCoordinatorManager)
+ .addInterfaceForward(anyString(), anyString());
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
- usbTeardownOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
+ InOrder usbTeardownOrder = inOrder(mNetd, mCallback, mRoutingCoordinatorManager);
+ usbTeardownOrder
+ .verify(mRoutingCoordinatorManager)
+ .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
@@ -765,8 +758,8 @@
lp.setInterfaceName(UPSTREAM_IFACE2);
lp.setLinkAddresses(UPSTREAM_ADDRESSES);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
reset(mBpfCoordinator);
// Upstream link addresses change result in updating the rules.
@@ -774,8 +767,8 @@
lp2.setInterfaceName(UPSTREAM_IFACE2);
lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
reset(mBpfCoordinator);
// When the upstream is lost, rules are removed.
@@ -784,53 +777,54 @@
// - processMessage CMD_TETHER_CONNECTION_CHANGED for the upstream is lost.
// - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
// See dispatchTetherConnectionChanged.
- verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verify(mBpfCoordinator, times(2)).updateIpv6UpstreamInterface(
+ mIpServer, NO_UPSTREAM, NO_PREFIXES);
reset(mBpfCoordinator);
// If the upstream is IPv4-only, no rules are added.
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- verify(mBpfCoordinator, never()).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verify(mBpfCoordinator, never()).updateIpv6UpstreamInterface(
+ mIpServer, NO_UPSTREAM, NO_PREFIXES);
reset(mBpfCoordinator);
// Rules are added again once upstream IPv6 connectivity is available.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
reset(mBpfCoordinator);
// If upstream IPv6 connectivity is lost, rules are removed.
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, NO_UPSTREAM, NO_PREFIXES);
reset(mBpfCoordinator);
// When upstream IPv6 connectivity comes back, rules are added.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
reset(mBpfCoordinator);
// When the downstream interface goes down, rules are removed.
mIpServer.stop();
mLooper.dispatchAll();
verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
- verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verify(mBpfCoordinator).removeIpServer(mIpServer);
+ verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+ mIpServer, NO_UPSTREAM, NO_PREFIXES);
reset(mBpfCoordinator);
}
@Test
- public void stopNeighborMonitoringWhenInterfaceDown() throws Exception {
+ public void removeIpServerWhenInterfaceDown() throws Exception {
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
mIpServer.stop();
mLooper.dispatchAll();
- verify(mIpNeighborMonitor).stop();
+ verify(mBpfCoordinator).removeIpServer(mIpServer);
}
private LinkProperties buildIpv6OnlyLinkProperties(final String iface) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 47ecf58..dd10cc3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -25,8 +25,6 @@
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
-import static android.net.TetheringManager.TETHERING_WIFI;
-import static android.net.ip.IpServer.STATE_TETHERED;
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
import static android.system.OsConstants.ETH_P_IP;
import static android.system.OsConstants.ETH_P_IPV6;
@@ -50,6 +48,7 @@
import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
+import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_METRICS_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU;
import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
@@ -62,6 +61,7 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import static com.android.testutils.MiscAsserts.assertSameElements;
import static org.junit.Assert.assertArrayEquals;
@@ -79,9 +79,7 @@
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
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;
@@ -91,6 +89,7 @@
import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.IpPrefix;
@@ -100,11 +99,9 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkStats;
-import android.net.RoutingCoordinatorManager;
import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
import android.net.ip.IpServer;
-import android.net.ip.RouterAdvertisementDaemon;
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
@@ -119,12 +116,10 @@
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
-import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.bpf.Tether4Key;
@@ -143,13 +138,13 @@
import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
-import com.android.networkstack.tethering.metrics.TetheringMetrics;
-import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestBpfMap;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
import org.junit.Before;
import org.junit.Rule;
@@ -181,6 +176,16 @@
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+ final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+ // This will set feature flags from @FeatureFlag annotations
+ // into the map before setUp() runs.
+ @Rule
+ public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+ new SetFeatureFlagsRule((name, enabled) -> {
+ mFeatureFlags.put(name, enabled);
+ return null;
+ }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
private static final boolean IPV4 = true;
private static final boolean IPV6 = false;
@@ -209,11 +214,6 @@
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
private static final MacAddress MAC_NULL = MacAddress.fromString("00:00:00:00:00:00");
- private static final LinkAddress UPSTREAM_ADDRESS = new LinkAddress("2001:db8:0:1234::168/64");
- private static final LinkAddress UPSTREAM_ADDRESS2 = new LinkAddress("2001:db8:0:abcd::168/64");
- private static final Set<LinkAddress> UPSTREAM_ADDRESSES = Set.of(UPSTREAM_ADDRESS);
- private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 =
- Set.of(UPSTREAM_ADDRESS, UPSTREAM_ADDRESS2);
private static final IpPrefix UPSTREAM_PREFIX = new IpPrefix("2001:db8:0:1234::/64");
private static final IpPrefix UPSTREAM_PREFIX2 = new IpPrefix("2001:db8:0:abcd::/64");
private static final Set<IpPrefix> UPSTREAM_PREFIXES = Set.of(UPSTREAM_PREFIX);
@@ -421,6 +421,11 @@
return this;
}
+ public Builder setPrivateAddress(Inet4Address privateAddr) {
+ mPrivateAddr = privateAddr;
+ return this;
+ }
+
public Builder setRemotePort(int remotePort) {
mRemotePort = (short) remotePort;
return this;
@@ -444,18 +449,12 @@
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
+ @Mock private Context mMockContext;
@Mock private IpServer mIpServer;
@Mock private IpServer mIpServer2;
@Mock private TetheringConfiguration mTetherConfig;
@Mock private ConntrackMonitor mConntrackMonitor;
@Mock private IpNeighborMonitor mIpNeighborMonitor;
- @Mock private RouterAdvertisementDaemon mRaDaemon;
- @Mock private IpServer.Dependencies mIpServerDeps;
- @Mock private IpServer.Callback mIpServerCallback;
- @Mock private PrivateAddressCoordinator mAddressCoordinator;
- private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
- new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
- @Mock private TetheringMetrics mTetheringMetrics;
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -497,6 +496,11 @@
}
@NonNull
+ public Context getContext() {
+ return mMockContext;
+ }
+
+ @NonNull
public INetd getNetd() {
return mNetd;
}
@@ -568,29 +572,23 @@
public IBpfMap<S32, S32> getBpfErrorMap() {
return mBpfErrorMap;
}
+
+ @Override
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ // No-op.
+ }
+
+ @Override
+ public boolean isFeatureEnabled(Context context, String name) {
+ return mFeatureFlags.getOrDefault(name, false);
+ }
});
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
-
- // Simulate the behavior of RoutingCoordinator
- if (null != mRoutingCoordinatorManager.value) {
- doAnswer(it -> {
- final String fromIface = (String) it.getArguments()[0];
- final String toIface = (String) it.getArguments()[1];
- mNetd.tetherAddForward(fromIface, toIface);
- mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
- return null;
- }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
- doAnswer(it -> {
- final String fromIface = (String) it.getArguments()[0];
- final String toIface = (String) it.getArguments()[1];
- mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
- mNetd.tetherRemoveForward(fromIface, toIface);
- return null;
- }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
- }
+ when(mIpServer.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS);
+ when(mIpServer2.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS2);
}
private void waitForIdle() {
@@ -603,70 +601,39 @@
when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
}
- @NonNull
- private IpServer makeAndStartIpServer(String interfaceName, BpfCoordinator bpfCoordinator)
- throws Exception {
- final LinkAddress testAddress = new LinkAddress("192.168.42.5/24");
- when(mIpServerDeps.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
- when(mIpServerDeps.getInterfaceParams(DOWNSTREAM_IFACE)).thenReturn(
- DOWNSTREAM_IFACE_PARAMS);
- when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
- when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
- when(mIpServerDeps.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS);
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
- anyBoolean())).thenReturn(testAddress);
- when(mRaDaemon.start()).thenReturn(true);
- ArgumentCaptor<NeighborEventConsumer> neighborEventCaptor =
- ArgumentCaptor.forClass(NeighborEventConsumer.class);
- doReturn(mIpNeighborMonitor).when(mIpServerDeps).getIpNeighborMonitor(any(), any(),
- neighborEventCaptor.capture());
- final IpServer ipServer = new IpServer(
- interfaceName, mHandler, TETHERING_WIFI, new SharedLog("test"), mNetd,
- bpfCoordinator, mRoutingCoordinatorManager, mIpServerCallback, mTetherConfig,
- mAddressCoordinator, mTetheringMetrics, mIpServerDeps);
- ipServer.start();
- ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
- mTestLooper.dispatchAll();
-
- LinkProperties lp = new LinkProperties();
- lp.setInterfaceName(UPSTREAM_IFACE);
- lp.setLinkAddresses(UPSTREAM_ADDRESSES);
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, 0);
-
- mNeighborEventConsumer = neighborEventCaptor.getValue();
- return ipServer;
- }
-
- private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface,
- LinkProperties v6lp, int ttlAdjustment) {
- dispatchTetherConnectionChanged(ipServer, upstreamIface);
- ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, ttlAdjustment, 0, v6lp);
- mTestLooper.dispatchAll();
- }
-
- private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface) {
- final InterfaceSet ifs = (upstreamIface != null) ? new InterfaceSet(upstreamIface) : null;
- ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifs);
- mTestLooper.dispatchAll();
+ private void dispatchIpv6UpstreamChanged(BpfCoordinator bpfCoordinator, IpServer ipServer,
+ int upstreamIfindex, String upstreamIface, Set<IpPrefix> upstreamPrefixes) {
+ bpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfindex, upstreamIface);
+ bpfCoordinator.updateIpv6UpstreamInterface(ipServer, upstreamIfindex, upstreamPrefixes);
+ when(ipServer.getIpv6UpstreamIfindex()).thenReturn(upstreamIfindex);
+ when(ipServer.getIpv6UpstreamPrefixes()).thenReturn(upstreamPrefixes);
}
private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
nudState, mac));
- mTestLooper.dispatchAll();
}
private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
nudState, mac));
- mTestLooper.dispatchAll();
}
@NonNull
private BpfCoordinator makeBpfCoordinator() throws Exception {
+ return makeBpfCoordinator(true /* addDefaultIpServer */);
+ }
+
+ @NonNull
+ private BpfCoordinator makeBpfCoordinator(boolean addDefaultIpServer) throws Exception {
// mStatsManager will be invoked twice if BpfCoordinator is created the second time.
clearInvocations(mStatsManager);
+ ArgumentCaptor<NeighborEventConsumer> neighborCaptor =
+ ArgumentCaptor.forClass(NeighborEventConsumer.class);
+ doReturn(mIpNeighborMonitor).when(mDeps).getIpNeighborMonitor(neighborCaptor.capture());
final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
+ mNeighborEventConsumer = neighborCaptor.getValue();
+ assertNotNull(mNeighborEventConsumer);
mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
mTetherClients = coordinator.getTetherClientsForTesting();
@@ -681,6 +648,10 @@
mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
+ if (addDefaultIpServer) {
+ coordinator.addIpServer(mIpServer);
+ }
+
return coordinator;
}
@@ -1008,7 +979,6 @@
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
- coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// InOrder is required because mBpfStatsMap may be accessed by both
// BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
@@ -1020,19 +990,18 @@
mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
mobileIfIndex, NEIGH_A, MAC_A);
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, upstreamRule);
- coordinator.addIpv6DownstreamRule(mIpServer, downstreamRule);
+ recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
verifyAddDownstreamRule(inOrder, downstreamRule);
// Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
verifyRemoveDownstreamRule(inOrder, downstreamRule);
verifyRemoveUpstreamRule(inOrder, upstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
@@ -1056,7 +1025,6 @@
doReturn(usingApiS).when(mDeps).isAtLeastS();
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.startPolling();
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
@@ -1092,7 +1060,6 @@
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.startPolling();
final String wlanIface = "wlan0";
final Integer wlanIfIndex = 100;
@@ -1148,7 +1115,7 @@
// [3] Stop coordinator.
// Shutdown the coordinator and clear the invocation history, especially the
// tetherOffloadGetStats() calls.
- coordinator.stopPolling();
+ coordinator.removeIpServer(mIpServer);
clearStatsInvocations();
// Verify the polling update thread stopped.
@@ -1162,7 +1129,6 @@
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.startPolling();
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
@@ -1350,7 +1316,6 @@
final String mobileIface = "rmnet_data0";
final int mobileIfIndex = 100;
- coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// [1] Default limit.
// Set the unlimited quota as default if the service has never applied a data limit for a
@@ -1358,8 +1323,8 @@
final Ipv6UpstreamRule rule = buildTestUpstreamRule(
mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, rule);
@@ -1395,7 +1360,6 @@
final String mobileIface = "rmnet_data0";
final int mobileIfIndex = 100;
- coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// Applying a data limit to the current upstream does not take any immediate action.
// The data limit could be only set on an upstream which has rules.
@@ -1408,31 +1372,30 @@
// Adding the first rule on current upstream immediately sends the quota to BPF.
final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
verifyAddUpstreamRule(inOrder, ruleA);
inOrder.verifyNoMoreInteractions();
// Adding the second rule on current upstream does not send the quota to BPF.
+ coordinator.addIpServer(mIpServer2);
final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
mobileIfIndex, DOWNSTREAM_IFINDEX2, UPSTREAM_PREFIX, DOWNSTREAM_MAC2);
- coordinator.updateAllIpv6Rules(
- mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex, UPSTREAM_PREFIXES);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer2, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
verifyAddUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the second rule on current upstream does not send the quota to BPF.
- coordinator.updateAllIpv6Rules(
- mIpServer2, DOWNSTREAM_IFACE_PARAMS2, NO_UPSTREAM, NO_PREFIXES);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer2, NO_UPSTREAM, null, NO_PREFIXES);
verifyRemoveUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
verifyRemoveUpstreamRule(inOrder, ruleA);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
inOrder.verifyNoMoreInteractions();
@@ -1448,8 +1411,6 @@
final String mobileIface = "rmnet_data0";
final Integer ethIfIndex = 100;
final Integer mobileIfIndex = 101;
- coordinator.maybeAddUpstreamToLookupTable(ethIfIndex, ethIface);
- coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
mBpfStatsMap);
@@ -1470,14 +1431,14 @@
final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
ethIfIndex, NEIGH_B, MAC_B);
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex, UPSTREAM_PREFIXES);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, ethIfIndex, ethIface, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
- coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
+ recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
verifyAddDownstreamRule(inOrder, ethernetRuleA);
- coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleB);
+ recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_B, NUD_REACHABLE, MAC_B);
verifyAddDownstreamRule(inOrder, ethernetRuleB);
// [2] Update the existing rules from Ethernet to cellular.
@@ -1494,8 +1455,8 @@
// Update the existing rules for upstream changes. The rules are removed and re-added one
// by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES2);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES2);
verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
@@ -1532,7 +1493,6 @@
// #makeBpfCoordinator for testing.
// See #testBpfDisabledbyNoBpfDownstream6Map.
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.startPolling();
// The tether stats polling task should not be scheduled.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
@@ -1549,7 +1509,7 @@
final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
final Ipv6DownstreamRule rule = buildTestDownstreamRule(ifIndex, neigh, mac);
- coordinator.addIpv6DownstreamRule(mIpServer, rule);
+ recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_REACHABLE, mac);
verifyNeverAddDownstreamRule();
LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1561,7 +1521,7 @@
rules = new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>();
rules.put(rule.address, rule);
coordinator.getIpv6DownstreamRulesForTesting().put(mIpServer, rules);
- coordinator.removeIpv6DownstreamRule(mIpServer, rule);
+ recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_STALE, mac);
verifyNeverRemoveDownstreamRule();
rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
assertNotNull(rules);
@@ -1575,8 +1535,8 @@
assertEquals(1, rules.size());
// The rule can't be updated.
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS,
- rule.upstreamIfindex + 1 /* new */, UPSTREAM_PREFIXES);
+ coordinator.updateIpv6UpstreamInterface(mIpServer, rule.upstreamIfindex + 1 /* new */,
+ UPSTREAM_PREFIXES);
verifyNeverRemoveDownstreamRule();
verifyNeverAddDownstreamRule();
rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1753,18 +1713,14 @@
final BpfCoordinator coordinator = makeBpfCoordinator();
// [1] The default polling interval.
- coordinator.startPolling();
assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
- coordinator.stopPolling();
// [2] Expect the invalid polling interval isn't applied. The valid range of interval is
// DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long.
for (final int interval
: new int[] {0, 100, DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS - 1}) {
when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
- coordinator.startPolling();
assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
- coordinator.stopPolling();
}
// [3] Set a specific polling interval which is larger than default value.
@@ -1772,7 +1728,6 @@
// approximation is used to verify the scheduled time of the polling thread.
final int pollingInterval = 100_000;
when(mTetherConfig.getOffloadPollInterval()).thenReturn(pollingInterval);
- coordinator.startPolling();
// Expect the specific polling interval to be applied.
assertEquals(pollingInterval, coordinator.getPollingInterval());
@@ -1800,19 +1755,19 @@
public void testStartStopConntrackMonitoring() throws Exception {
setupFunctioningNetdInterface();
- final BpfCoordinator coordinator = makeBpfCoordinator();
+ final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
// [1] Don't stop monitoring if it has never started.
- coordinator.stopMonitoring(mIpServer);
+ coordinator.removeIpServer(mIpServer);
verify(mConntrackMonitor, never()).stop();
// [2] Start monitoring.
- coordinator.startMonitoring(mIpServer);
+ coordinator.addIpServer(mIpServer);
verify(mConntrackMonitor).start();
clearInvocations(mConntrackMonitor);
// [3] Stop monitoring.
- coordinator.stopMonitoring(mIpServer);
+ coordinator.removeIpServer(mIpServer);
verify(mConntrackMonitor).stop();
}
@@ -1823,12 +1778,12 @@
public void testStartStopConntrackMonitoring_R() throws Exception {
setupFunctioningNetdInterface();
- final BpfCoordinator coordinator = makeBpfCoordinator();
+ final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
- coordinator.startMonitoring(mIpServer);
+ coordinator.addIpServer(mIpServer);
verify(mConntrackMonitor, never()).start();
- coordinator.stopMonitoring(mIpServer);
+ coordinator.removeIpServer(mIpServer);
verify(mConntrackMonitor, never()).stop();
}
@@ -1837,23 +1792,23 @@
public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception {
setupFunctioningNetdInterface();
- final BpfCoordinator coordinator = makeBpfCoordinator();
+ final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
// [1] Start monitoring at the first IpServer adding.
- coordinator.startMonitoring(mIpServer);
+ coordinator.addIpServer(mIpServer);
verify(mConntrackMonitor).start();
clearInvocations(mConntrackMonitor);
// [2] Don't start monitoring at the second IpServer adding.
- coordinator.startMonitoring(mIpServer2);
+ coordinator.addIpServer(mIpServer2);
verify(mConntrackMonitor, never()).start();
// [3] Don't stop monitoring if any downstream interface exists.
- coordinator.stopMonitoring(mIpServer2);
+ coordinator.removeIpServer(mIpServer2);
verify(mConntrackMonitor, never()).stop();
// [4] Stop monitoring if no downstream exists.
- coordinator.stopMonitoring(mIpServer);
+ coordinator.removeIpServer(mIpServer);
verify(mConntrackMonitor).stop();
}
@@ -2016,9 +1971,8 @@
public void testAddDevMapRule6() throws Exception {
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
eq(new TetherDevValue(UPSTREAM_IFINDEX)));
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
@@ -2027,8 +1981,9 @@
// Adding the second downstream, only the second downstream ifindex is added to DevMap,
// the existing upstream ifindex won't be added again.
- coordinator.updateAllIpv6Rules(
- mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ coordinator.addIpServer(mIpServer2);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer2, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
@@ -2058,6 +2013,217 @@
verify(mBpfDevMap, never()).updateEntry(any(), any());
}
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testMaxConnectionCount_metricsEnabled() throws Exception {
+ doTestMaxConnectionCount(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testMaxConnectionCount_metricsDisabled() throws Exception {
+ doTestMaxConnectionCount(false);
+ }
+
+ private void doTestMaxConnectionCount(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Prepare add/delete rule events.
+ final ArrayList<ConntrackEvent> addRuleEvents = new ArrayList<>();
+ final ArrayList<ConntrackEvent> delRuleEvents = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ final ConntrackEvent addEvent = new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build();
+ addRuleEvents.add(addEvent);
+ final ConntrackEvent delEvent = new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build();
+ delRuleEvents.add(delEvent);
+ }
+
+ // Add rules, verify counter increases.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(addRuleEvents.get(i));
+ assertEquals(supportActiveSessionsMetrics ? i + 1 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Add the same events again should not increase the counter because
+ // all events are already exist.
+ for (final ConntrackEvent event : addRuleEvents) {
+ mConsumer.accept(event);
+ assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Verify removing non-existent items won't change the counters.
+ for (int i = 5; i < 8; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Verify remove the rules decrease the counter.
+ // Note the max counter returns the max, so it returns the count before deleting.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(delRuleEvents.get(i));
+ }
+ // The maximum number of rules observed is still 5.
+ assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ // After the reset, the maximum number of rules observed is 0.
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Verify remove these rules again doesn't decrease the counter.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(delRuleEvents.get(i));
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void doTestMaxConnectionCount_removeClient_metricsEnabled() throws Exception {
+ doTestMaxConnectionCount_removeClient(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void doTestMaxConnectionCount_removeClient_metricsDisabled() throws Exception {
+ doTestMaxConnectionCount_removeClient(false);
+ }
+
+ private void doTestMaxConnectionCount_removeClient(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+
+ // Add client information A and B on on the same downstream.
+ final ClientInfo clientA = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR, MAC_A);
+ final ClientInfo clientB = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR2, MAC_B);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientA);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientB);
+ assertClientInfoExists(mIpServer, clientA);
+ assertClientInfoExists(mIpServer, clientB);
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Add some rules for both clients.
+ final int addr1RuleCount = 5;
+ final int addr2RuleCount = 3;
+
+ for (int i = 0; i < addr1RuleCount; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder()
+ .setMsgType(IPCTNL_MSG_CT_NEW)
+ .setProto(IPPROTO_TCP)
+ .setRemotePort(i)
+ .setPrivateAddress(PRIVATE_ADDR)
+ .build());
+ }
+
+ for (int i = addr1RuleCount; i < addr1RuleCount + addr2RuleCount; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder()
+ .setMsgType(IPCTNL_MSG_CT_NEW)
+ .setProto(IPPROTO_TCP)
+ .setRemotePort(i)
+ .setPrivateAddress(PRIVATE_ADDR2)
+ .build());
+ }
+
+ assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Remove 1 client. Since the 1st poll will return the LastMaxCounter and
+ // update it to the current, the max counter will be kept at 1st poll.
+ coordinator.tetherOffloadClientRemove(mIpServer, clientA);
+ assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ // And the counter be updated at 2nd poll.
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Remove other client.
+ coordinator.tetherOffloadClientRemove(mIpServer, clientB);
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ // Verify the counter reach zero at 2nd poll.
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testSendActiveSessionsReported_metricsEnabled() throws Exception {
+ doTestSendActiveSessionsReported(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testSendActiveSessionsReported_metricsDisabled() throws Exception {
+ doTestSendActiveSessionsReported(false);
+ }
+
+ private void doTestSendActiveSessionsReported(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Prepare the counter value.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Then delete some 3 rules, 2 rules remaining.
+ // The max count is 5 while current rules count is 2.
+ for (int i = 0; i < 3; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Verify the method is not invoked when timer is not expired.
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+
+ // Verify metrics will be sent upon timer expiry.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(5);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify next uploaded metrics will reflect the decreased rules count.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(2);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify no metrics uploaded if polling stopped.
+ clearInvocations(mDeps);
+ coordinator.removeIpServer(mIpServer);
+ mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
private void setElapsedRealtimeNanos(long nanoSec) {
mElapsedRealtimeNanos = nanoSec;
}
@@ -2087,7 +2253,6 @@
.startMocking();
try {
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.startPolling();
bpfMap.insertEntry(tcpKey, tcpValue);
bpfMap.insertEntry(udpKey, udpValue);
@@ -2116,7 +2281,7 @@
ExtendedMockito.clearInvocations(staticMockMarker(NetlinkUtils.class));
// [3] Don't refresh conntrack timeout if polling stopped.
- coordinator.stopPolling();
+ coordinator.removeIpServer(mIpServer);
mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
waitForIdle();
ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkUtils.class));
@@ -2855,8 +3020,9 @@
final int myIfindex = DOWNSTREAM_IFINDEX;
final int notMyIfindex = myIfindex - 1;
final BpfCoordinator coordinator = makeBpfCoordinator();
- final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
resetNetdAndBpfMaps();
verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
@@ -2905,10 +3071,8 @@
resetNetdAndBpfMaps();
InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
- LinkProperties lp = new LinkProperties();
- lp.setInterfaceName(UPSTREAM_IFACE2);
- lp.setLinkAddresses(UPSTREAM_ADDRESSES);
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp, -1);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES);
final Ipv6DownstreamRule ruleA2 = buildTestDownstreamRule(
UPSTREAM_IFINDEX2, NEIGH_A, MAC_A);
final Ipv6DownstreamRule ruleB2 = buildTestDownstreamRule(
@@ -2922,11 +3086,9 @@
verifyNoUpstreamIpv6ForwardingChange(inOrder);
resetNetdAndBpfMaps();
- // Upstream link addresses change result in updating the rules.
- LinkProperties lp2 = new LinkProperties();
- lp2.setInterfaceName(UPSTREAM_IFACE2);
- lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, -1);
+ // Upstream prefixes change result in updating the rules.
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
verifyRemoveDownstreamRule(inOrder, ruleA2);
verifyRemoveDownstreamRule(inOrder, ruleB2);
verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
@@ -2936,7 +3098,7 @@
resetNetdAndBpfMaps();
// When the upstream is lost, rules are removed.
- dispatchTetherConnectionChanged(ipServer, null, null, 0);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
verifyRemoveDownstreamRule(ruleA2);
verifyRemoveDownstreamRule(ruleB2);
@@ -2947,7 +3109,7 @@
resetNetdAndBpfMaps();
// If the upstream is IPv4-only, no IPv6 rules are added to BPF map.
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
resetNetdAndBpfMaps();
recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
verifyNoUpstreamIpv6ForwardingChange(null);
@@ -2957,8 +3119,8 @@
// Rules can be added again once upstream IPv6 connectivity is available. The existing rules
// with an upstream of NO_UPSTREAM are reapplied.
- lp.setInterfaceName(UPSTREAM_IFACE);
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verifyAddDownstreamRule(ruleA);
recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
@@ -2966,23 +3128,27 @@
// If upstream IPv6 connectivity is lost, rules are removed.
resetNetdAndBpfMaps();
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, null, 0);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
verifyRemoveDownstreamRule(ruleA);
verifyRemoveDownstreamRule(ruleB);
verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
// When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
// are reapplied.
- lp.setInterfaceName(UPSTREAM_IFACE);
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verifyAddDownstreamRule(ruleA);
verifyAddDownstreamRule(ruleB);
resetNetdAndBpfMaps();
// When the downstream interface goes down, rules are removed.
- ipServer.stop();
- mTestLooper.dispatchAll();
+ // Simulate receiving CMD_INTERFACE_DOWN in the BaseServingState of IpServer.
+ reset(mIpNeighborMonitor);
+ dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+ coordinator.tetherOffloadClientClear(mIpServer);
+ coordinator.removeIpServer(mIpServer);
+
verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
verifyRemoveDownstreamRule(ruleA);
verifyRemoveDownstreamRule(ruleB);
@@ -3004,7 +3170,8 @@
// [1] Enable BPF offload.
// A neighbor that is added or deleted causes the rule to be added or removed.
final BpfCoordinator coordinator = makeBpfCoordinator();
- final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
resetNetdAndBpfMaps();
recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
@@ -3019,10 +3186,8 @@
resetNetdAndBpfMaps();
// Upstream IPv6 connectivity change causes upstream rules change.
- LinkProperties lp2 = new LinkProperties();
- lp2.setInterfaceName(UPSTREAM_IFACE2);
- lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
- dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, 0);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
resetNetdAndBpfMaps();
@@ -3030,7 +3195,6 @@
// A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
final BpfCoordinator coordinator2 = makeBpfCoordinator();
- final IpServer ipServer2 = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator2);
verifyNoUpstreamIpv6ForwardingChange(null);
resetNetdAndBpfMaps();
@@ -3043,7 +3207,8 @@
resetNetdAndBpfMaps();
// Upstream IPv6 connectivity change doesn't cause the rule to be added or removed.
- dispatchTetherConnectionChanged(ipServer2, UPSTREAM_IFACE2, lp2, 0);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer2, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, NO_PREFIXES);
verifyNoUpstreamIpv6ForwardingChange(null);
verifyNeverRemoveDownstreamRule();
resetNetdAndBpfMaps();
@@ -3053,7 +3218,6 @@
public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
final BpfCoordinator coordinator = makeBpfCoordinator();
- final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
// IP neighbor monitor doesn't start if BPF offload is disabled.
verify(mIpNeighborMonitor, never()).start();
@@ -3062,15 +3226,10 @@
@Test
public void testSkipVirtualNetworkInBpf() throws Exception {
final BpfCoordinator coordinator = makeBpfCoordinator();
- final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
- final LinkProperties v6Only = new LinkProperties();
- v6Only.setInterfaceName(IPSEC_IFACE);
- v6Only.setLinkAddresses(UPSTREAM_ADDRESSES);
resetNetdAndBpfMaps();
- dispatchTetherConnectionChanged(ipServer, IPSEC_IFACE, v6Only, 0);
- verify(mNetd).tetherAddForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
- verify(mNetd).ipfwdAddInterfaceForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, IPSEC_IFINDEX, IPSEC_IFACE, UPSTREAM_PREFIXES);
verifyNeverAddUpstreamRule();
recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
@@ -3080,7 +3239,6 @@
@Test
public void addRemoveTetherClient() throws Exception {
final BpfCoordinator coordinator = makeBpfCoordinator();
- final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
final int myIfindex = DOWNSTREAM_IFINDEX;
final int notMyIfindex = myIfindex - 1;
@@ -3089,33 +3247,36 @@
final InetAddress neighLL = InetAddresses.parseNumericAddress("169.254.0.1");
final InetAddress neighMC = InetAddresses.parseNumericAddress("224.0.0.1");
+ dispatchIpv6UpstreamChanged(
+ coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
+
// Events on other interfaces are ignored.
recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, MAC_A);
- assertNull(mTetherClients.get(ipServer));
+ assertNull(mTetherClients.get(mIpServer));
// Events on this interface are received and sent to BpfCoordinator.
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, MAC_A);
- assertClientInfoExists(ipServer,
+ assertClientInfoExists(mIpServer,
new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighA, MAC_A));
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, MAC_B);
- assertClientInfoExists(ipServer,
+ assertClientInfoExists(mIpServer,
new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighB, MAC_B));
// Link-local and multicast neighbors are ignored.
recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, MAC_A);
- assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighLL);
+ assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighLL);
recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, MAC_A);
- assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighMC);
+ assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighMC);
// A neighbor that is no longer valid causes the client to be removed.
// NUD_FAILED events do not have a MAC address.
recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
- assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighA);
+ assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighA);
// A neighbor that is deleted causes the client to be removed.
recvDelNeigh(myIfindex, neighB, NUD_STALE, MAC_B);
// When last client information is deleted, IpServer will be removed from mTetherClients
- assertNull(mTetherClients.get(ipServer));
+ assertNull(mTetherClients.get(mIpServer));
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 2298a1a..a5c06f3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -16,7 +16,6 @@
package com.android.networkstack.tethering;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
@@ -29,9 +28,11 @@
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -103,11 +104,17 @@
MockitoAnnotations.initMocks(this);
when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
+ when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityMgr);
when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
setUpIpServers();
- mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+ mPrivateAddressCoordinator =
+ spy(
+ new PrivateAddressCoordinator(
+ mConnectivityMgr::getAllNetworks,
+ mConfig.isRandomPrefixBaseEnabled(),
+ mConfig.shouldEnableWifiP2pDedicatedIp()));
}
private LinkAddress requestDownstreamAddress(final IpServer ipServer, int scope,
@@ -118,6 +125,11 @@
return address;
}
+ private void updateUpstreamPrefix(UpstreamNetworkState ns) {
+ mPrivateAddressCoordinator.updateUpstreamPrefix(
+ ns.linkProperties, ns.networkCapabilities, ns.network);
+ }
+
@Test
public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception {
final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress);
@@ -143,37 +155,6 @@
}
@Test
- public void testSanitizedAddress() throws Exception {
- int fakeSubAddr = 0x2b00; // 43.0.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2d01; // 45.1.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2eff; // 46.255.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2f05; // 47.5.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- }
-
- @Test
public void testReservedPrefix() throws Exception {
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -208,22 +189,15 @@
@Test
public void testRequestLastDownstreamAddress() throws Exception {
- final int fakeHotspotSubAddr = 0x2b05; // 43.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
- final int newFakeSubAddr = 0x3c05;
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
-
final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals(hotspotAddress, newHotspotAddress);
@@ -234,7 +208,7 @@
final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
new LinkAddress("192.168.88.23/16"), null,
makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
+ updateUpstreamPrefix(wifiUpstream);
verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
verify(mUsbIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
}
@@ -261,262 +235,27 @@
}
@Test
- public void testNoConflictUpstreamPrefix() throws Exception {
- final int fakeHotspotSubAddr = 0x2b05; // 43.5
- final IpPrefix predefinedPrefix = new IpPrefix("192.168.43.0/24");
- // Force always get subAddress "43.5" for conflict testing.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
- // - Enable hotspot with prefix 192.168.43.0/24
- final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr);
- assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix);
- // - test mobile network with null NetworkCapabilities. Ideally this should not happen
- // because NetworkCapabilities update should always happen before LinkProperties update
- // and the UpstreamNetworkState update, just make sure no crash in this case.
- final UpstreamNetworkState noCapUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), null, null);
- mPrivateAddressCoordinator.updateUpstreamPrefix(noCapUpstream);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - test mobile upstream with no address.
- final UpstreamNetworkState noAddress = buildUpstreamNetworkState(mMobileNetwork,
- null, null, makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(noCapUpstream);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v6 only mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v6OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
- null, new LinkAddress("2001:db8::/64"),
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v6OnlyMobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.removeUpstreamPrefix(mMobileNetwork);
- // - Update v4 only mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v4OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyMobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v4v6 mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v4v6Mobile = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), new LinkAddress("2001:db8::/64"),
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4v6Mobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v6 only wifi network, hotspot prefix should not be removed.
- final UpstreamNetworkState v6OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
- null, new LinkAddress("2001:db8::/64"), makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v6OnlyWifi);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
- // - Update vpn network, it conflict with hotspot prefix but VPN networks are ignored.
- final UpstreamNetworkState v4OnlyVpn = buildUpstreamNetworkState(mVpnNetwork,
- new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_VPN));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyVpn);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v4 only wifi network, it conflict with hotspot prefix.
- final UpstreamNetworkState v4OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi);
- verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- reset(mHotspotIpServer);
- // - Restart hotspot again and its prefix is different previous.
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2);
- assertNotEquals(hotspotPrefix, hotspotPrefix2);
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Usb tethering can be enabled and its prefix is different with conflict one.
- final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix usbPrefix = asIpPrefix(usbAddr);
- assertNotEquals(predefinedPrefix, usbPrefix);
- assertNotEquals(hotspotPrefix2, usbPrefix);
- // - Disable wifi upstream, then wifi's prefix can be selected again.
- mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
- final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix ethPrefix = asIpPrefix(ethAddr);
- assertEquals(predefinedPrefix, ethPrefix);
+ public void testChooseDownstreamAddress_noUpstreamConflicts() throws Exception {
+ LinkAddress address = new LinkAddress("192.168.42.42/24");
+ UpstreamNetworkState ns = buildUpstreamNetworkState(mMobileNetwork, address, null, null);
+ updateUpstreamPrefix(ns);
+ // try to look for a /24 in upstream that does not conflict with upstream -> impossible.
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(asIpPrefix(address)));
+
+ IpPrefix prefix = new IpPrefix("192.168.0.0/16");
+ LinkAddress chosenAddress = mPrivateAddressCoordinator.chooseDownstreamAddress(prefix);
+ assertNotNull(chosenAddress);
+ assertTrue(prefix.containsPrefix(asIpPrefix(chosenAddress)));
}
@Test
- public void testChooseAvailablePrefix() throws Exception {
- final int randomAddress = 0x8605; // 134.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
- final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5.
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0);
- final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.134.13/26"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
-
- // Check whether return address is next prefix of 192.168.134.0/24.
- final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1);
- final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.149.16/19"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream2);
-
-
- // The conflict range is 128 ~ 159, so the address is 192.168.160.5/24.
- final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2);
- final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("192.168.129.53/18"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- // Update another conflict upstream which is covered by the previous one (but not the first
- // one) and verify whether this would affect the result.
- final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
- new LinkAddress("192.168.170.7/19"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream);
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream2);
-
- // The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24.
- final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3);
- final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
- new LinkAddress("192.168.188.133/17"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream3);
-
- // Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because
- // 192.168.134/24 ~ 192.168.255.255/24 is not available.
- final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4);
- final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
- new LinkAddress("192.168.3.59/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream4);
-
- // Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24.
- final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5);
- final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
- new LinkAddress("192.168.68.43/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream5);
-
- // Update an upstream that does *not* conflict, check whether return the same address
- // 192.168.5/24.
- final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6);
- final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6,
- new LinkAddress("192.168.10.97/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream6);
-
- // Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24.
- final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7);
- final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6,
- new LinkAddress("192.168.0.0/17"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream7);
-
- // Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16.
- final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8);
- }
-
- @Test
- public void testChoosePrefixFromDifferentRanges() throws Exception {
- final int randomAddress = 0x1f2b2a; // 31.43.42
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
- final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42.
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1);
- final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.88.23/17"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // Check whether return address is next address of prefix 192.168.128.0/17.
- final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2);
- final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("192.1.2.3/8"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // Check whether return address is under prefix 172.16.0.0/12.
- final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1);
- final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
- new LinkAddress("172.28.123.100/14"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream2);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // 172.28.0.0 ~ 172.31.255.255 is not available.
- // Check whether return address is next address of prefix 172.16.0.0/14.
- final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2);
-
- // Check whether new downstream is next address of address 172.16.0.42/24.
- final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3);
- final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
- new LinkAddress("172.16.0.1/24"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream3);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verify(mUsbIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-
- // Check whether return address is next address of prefix 172.16.1.42/24.
- final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4);
- final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
- new LinkAddress("172.16.0.1/13"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream4);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verifyNotifyConflictAndRelease(mUsbIpServer);
-
- // Check whether return address is next address of prefix 172.16.0.1/13.
- final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5);
- // Check whether return address is next address of prefix 172.24.0.42/24.
- final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6);
- final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
- new LinkAddress("172.24.0.1/12"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream5);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verifyNotifyConflictAndRelease(mUsbIpServer);
-
- // Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42.
- final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1);
- // Check whether new downstream is next address of address 10.31.43.42/24.
- final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
+ public void testChooseDownstreamAddress_excludesWellKnownPrefixes() throws Exception {
+ IpPrefix prefix = new IpPrefix("192.168.0.0/24");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+ prefix = new IpPrefix("192.168.100.0/24");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+ prefix = new IpPrefix("10.3.0.0/16");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
}
private void verifyNotifyConflictAndRelease(final IpServer ipServer) throws Exception {
@@ -562,17 +301,18 @@
@Test
public void testEnableSapAndLohsConcurrently() throws Exception {
- // 0x2b05 -> 43.5, 0x8605 -> 134.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(0x2b05, 0x8605);
-
final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong hotspot prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+ assertNotNull(hotspotAddress);
final LinkAddress localHotspotAddress = requestDownstreamAddress(mLocalHotspotIpServer,
CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
- assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
- localHotspotAddress);
+ assertNotNull(localHotspotAddress);
+
+ final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
+ final IpPrefix localHotspotPrefix = asIpPrefix(localHotspotAddress);
+ assertFalse(hotspotPrefix.containsPrefix(localHotspotPrefix));
+ assertFalse(localHotspotPrefix.containsPrefix(hotspotPrefix));
}
@Test
@@ -602,7 +342,12 @@
private void startedPrefixBaseTest(final String expected, final int randomIntForPrefixBase)
throws Exception {
- mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+ mPrivateAddressCoordinator =
+ spy(
+ new PrivateAddressCoordinator(
+ mConnectivityMgr::getAllNetworks,
+ mConfig.isRandomPrefixBaseEnabled(),
+ mConfig.shouldEnableWifiP2pDedicatedIp()));
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index da81bda..c0d7ad4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -45,6 +45,7 @@
import android.net.ITetheringConnector;
import android.net.ITetheringEventCallback;
import android.net.TetheringManager;
+import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringRequestParcel;
import android.net.ip.IpServer;
import android.os.Bundle;
@@ -311,7 +312,8 @@
result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
+ verify(mTethering).startTethering(
+ eq(new TetheringRequest(request)), eq(TEST_CALLER_PKG), eq(result));
}
@Test
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 9f430af..9a4945e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -124,6 +124,7 @@
import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.EthernetManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -142,7 +143,6 @@
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.RouteInfo;
-import android.net.RoutingCoordinatorManager;
import android.net.TetherStatesParcel;
import android.net.TetheredClient;
import android.net.TetheredClient.AddressInfo;
@@ -150,7 +150,7 @@
import android.net.TetheringConfigurationParcel;
import android.net.TetheringInterface;
import android.net.TetheringManager;
-import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpLeaseParcelable;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
@@ -191,7 +191,7 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -292,6 +292,7 @@
@Mock private BluetoothPanShim mBluetoothPanShim;
@Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
@Mock private TetheringMetrics mTetheringMetrics;
+ @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies());
@@ -374,6 +375,7 @@
@Override
public String getSystemServiceName(Class<?> serviceClass) {
if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+ if (ConnectivityManager.class.equals(serviceClass)) return Context.CONNECTIVITY_SERVICE;
return super.getSystemServiceName(serviceClass);
}
}
@@ -484,10 +486,10 @@
return mEntitleMgr;
}
- @Nullable
@Override
- public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(final Context context) {
- return new LateSdk<>(null);
+ public RoutingCoordinatorManager getRoutingCoordinator(final Context context,
+ SharedLog log) {
+ return mRoutingCoordinatorManager;
}
@Override
@@ -498,7 +500,7 @@
}
@Override
- public INetd getINetd(Context context) {
+ public INetd getINetd(Context context, SharedLog log) {
return mNetd;
}
@@ -528,7 +530,7 @@
}
@Override
- public TetheringMetrics makeTetheringMetrics() {
+ public TetheringMetrics makeTetheringMetrics(Context ctx) {
return mTetheringMetrics;
}
@@ -743,22 +745,21 @@
doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
}
- private TetheringRequestParcel createTetheringRequestParcel(final int type) {
- return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
+ private TetheringRequest createTetheringRequest(final int type) {
+ return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
}
- private TetheringRequestParcel createTetheringRequestParcel(final int type,
- final LinkAddress serverAddr, final LinkAddress clientAddr, final boolean exempt,
- final int scope) {
- final TetheringRequestParcel request = new TetheringRequestParcel();
- request.tetheringType = type;
- request.localIPv4Address = serverAddr;
- request.staticClientAddress = clientAddr;
- request.exemptFromEntitlementCheck = exempt;
- request.showProvisioningUi = false;
- request.connectivityScope = scope;
-
- return request;
+ private TetheringRequest createTetheringRequest(final int type,
+ final LinkAddress localIPv4Address, final LinkAddress staticClientAddress,
+ final boolean exempt, final int scope) {
+ TetheringRequest.Builder builder = new TetheringRequest.Builder(type)
+ .setExemptFromEntitlementCheck(exempt)
+ .setConnectivityScope(scope)
+ .setShouldShowEntitlementUi(false);
+ if (localIPv4Address != null && staticClientAddress != null) {
+ builder.setStaticIpv4Addresses(localIPv4Address, staticClientAddress);
+ }
+ return builder.build();
}
@NonNull
@@ -911,7 +912,7 @@
private void prepareNcmTethering() {
// Emulate startTethering(TETHERING_NCM) called
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_NCM), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -919,7 +920,7 @@
private void prepareUsbTethering() {
// Emulate pressing the USB tethering button in Settings UI.
- final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
+ final TetheringRequest request = createTetheringRequest(TETHERING_USB);
mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
@@ -1083,8 +1084,8 @@
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
- verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
sendIPv6TetherUpdates(upstreamState);
assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */);
@@ -1112,8 +1113,8 @@
UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
runUsbTethering(upstreamState);
- verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
sendIPv6TetherUpdates(upstreamState);
// TODO: add interfaceParams to compare in verify.
@@ -1128,8 +1129,8 @@
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
runUsbTethering(upstreamState);
- verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
verify(mRouterAdvertisementDaemon, times(1)).start();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
@@ -1146,13 +1147,12 @@
UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
runUsbTethering(upstreamState);
- verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
- verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME,
- TEST_XLAT_MOBILE_IFNAME);
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
sendIPv6TetherUpdates(upstreamState);
assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
@@ -1173,10 +1173,10 @@
UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
runUsbTethering(upstreamState);
- verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
// Then 464xlat comes up
upstreamState = buildMobile464xlatUpstreamState();
@@ -1188,12 +1188,11 @@
mLooper.dispatchAll();
// Forwarding is added for 464xlat
- verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME,
- TEST_XLAT_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
// Forwarding was not re-added for v6 (still times(1))
- verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
- verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager, times(1))
+ .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
// DHCP not restarted on downstream (still times(1))
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
@@ -1909,7 +1908,7 @@
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).startTetheredHotspot(null);
@@ -1938,7 +1937,7 @@
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).startTetheredHotspot(null);
@@ -1988,7 +1987,7 @@
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).startTetheredHotspot(null);
@@ -2334,7 +2333,7 @@
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
tetherState = callback.pollTetherStatesChanged();
@@ -2430,11 +2429,11 @@
initTetheringOnTestThread();
final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_ETHERNET), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verify(mEm, times(1)).requestTetheredInterface(any(), any());
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_ETHERNET), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verifyNoMoreInteractions(mEm);
@@ -2644,7 +2643,7 @@
final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
// Enable USB tethering and check that Tethering starts USB.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_USB), TEST_CALLER_PKG,
firstResult);
mLooper.dispatchAll();
firstResult.assertHasResult();
@@ -2652,7 +2651,7 @@
verifyNoMoreInteractions(mUsbManager);
// Enable USB tethering again with the same request and expect no change to USB.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_USB), TEST_CALLER_PKG,
secondResult);
mLooper.dispatchAll();
secondResult.assertHasResult();
@@ -2661,7 +2660,7 @@
// Enable USB tethering with a different request and expect that USB is stopped and
// started.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
+ mTethering.startTethering(createTetheringRequest(TETHERING_USB,
serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
TEST_CALLER_PKG, thirdResult);
mLooper.dispatchAll();
@@ -2692,7 +2691,7 @@
final int clientAddrParceled = 0xc0a8002a;
final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
+ mTethering.startTethering(createTetheringRequest(TETHERING_USB,
serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
TEST_CALLER_PKG, null);
mLooper.dispatchAll();
@@ -2820,8 +2819,8 @@
public void testExemptFromEntitlementCheck() throws Exception {
initTetheringOnTestThread();
setupForRequiredProvisioning();
- final TetheringRequestParcel wifiNotExemptRequest =
- createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
+ final TetheringRequest wifiNotExemptRequest =
+ createTetheringRequest(TETHERING_WIFI, null, null, false,
CONNECTIVITY_SCOPE_GLOBAL);
mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
@@ -2834,8 +2833,8 @@
reset(mEntitleMgr);
setupForRequiredProvisioning();
- final TetheringRequestParcel wifiExemptRequest =
- createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
+ final TetheringRequest wifiExemptRequest =
+ createTetheringRequest(TETHERING_WIFI, null, null, true,
CONNECTIVITY_SCOPE_GLOBAL);
mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
@@ -2954,7 +2953,7 @@
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET),
+ mTethering.startTethering(createTetheringRequest(TETHERING_ETHERNET),
TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
@@ -3235,7 +3234,7 @@
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
TEST_CALLER_PKG, result);
mLooper.dispatchAll();
verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
@@ -3272,7 +3271,7 @@
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
TEST_CALLER_PKG, result);
mLooper.dispatchAll();
verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
@@ -3294,7 +3293,7 @@
// already bound.
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
TEST_CALLER_PKG, secondResult);
mLooper.dispatchAll();
verifySetBluetoothTethering(true /* enable */, false /* bindToPanService */);
@@ -3317,7 +3316,7 @@
initTetheringOnTestThread();
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
TEST_CALLER_PKG, result);
mLooper.dispatchAll();
ServiceListener panListener = verifySetBluetoothTethering(true /* enable */,
@@ -3436,8 +3435,7 @@
runUsbTethering(upstreamState);
verify(mNetd).interfaceGetList();
- verify(mNetd).tetherAddForward(expectedIface, TEST_MOBILE_IFNAME);
- verify(mNetd).ipfwdAddInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
+ verify(mRoutingCoordinatorManager).addInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
verify(mRouterAdvertisementDaemon).start();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
@@ -3487,7 +3485,7 @@
// If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
// available.
final ResultListener ncmResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_NCM), TEST_CALLER_PKG,
ncmResult);
mLooper.dispatchAll();
ncmResult.assertHasResult();
@@ -3638,7 +3636,7 @@
when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
verify(mWifiManager).startTetheredHotspot(null);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index e2c924c..6b646ec 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -16,12 +16,23 @@
package com.android.networkstack.tethering.metrics;
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_NCM;
@@ -45,59 +56,90 @@
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+import static android.stats.connectivity.UpstreamType.UT_BLUETOOTH;
+import static android.stats.connectivity.UpstreamType.UT_CELLULAR;
+import static android.stats.connectivity.UpstreamType.UT_ETHERNET;
+import static android.stats.connectivity.UpstreamType.UT_WIFI;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
+import static com.android.networkstack.tethering.metrics.TetheringMetrics.EMPTY;
+import static com.android.testutils.NetworkStatsUtilsKt.makePublicStatsFromAndroidNetStats;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.NetworkCapabilities;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.stats.connectivity.DownstreamType;
import android.stats.connectivity.ErrorCode;
import android.stats.connectivity.UpstreamType;
import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.DataUsage;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
public final class TetheringMetricsTest {
+ @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
private static final String SETTINGS_PKG = "com.android.settings";
private static final String SYSTEMUI_PKG = "com.android.systemui";
private static final String GMS_PKG = "com.google.android.gms";
private static final long TEST_START_TIME = 1670395936033L;
private static final long SECOND_IN_MILLIS = 1_000L;
+ private static final long DEFAULT_TIMEOUT = 2000L;
+ private static final int MATCH_NONE = -1;
+
+ @Mock private Context mContext;
+ @Mock private Dependencies mDeps;
+ @Mock private NetworkStatsManager mNetworkStatsManager;
private TetheringMetrics mTetheringMetrics;
private final NetworkTetheringReported.Builder mStatsBuilder =
NetworkTetheringReported.newBuilder();
+ private final ArrayMap<UpstreamType, DataUsage> mMockUpstreamUsageBaseline = new ArrayMap<>();
+ private HandlerThread mThread;
+ private Handler mHandler;
private long mElapsedRealtime;
- private class MockTetheringMetrics extends TetheringMetrics {
- @Override
- public void write(final NetworkTetheringReported reported) {}
- @Override
- public long timeNow() {
- return currentTimeMillis();
- }
- }
-
private long currentTimeMillis() {
return TEST_START_TIME + mElapsedRealtime;
}
private void incrementCurrentTime(final long duration) {
mElapsedRealtime += duration;
- mTetheringMetrics.timeNow();
+ final long currentTimeMillis = currentTimeMillis();
+ doReturn(currentTimeMillis).when(mDeps).timeNow();
}
private long getElapsedRealtime() {
@@ -106,15 +148,42 @@
private void clearElapsedRealtime() {
mElapsedRealtime = 0;
+ doReturn(TEST_START_TIME).when(mDeps).timeNow();
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mTetheringMetrics = spy(new MockTetheringMetrics());
+ doReturn(TEST_START_TIME).when(mDeps).timeNow();
+ doReturn(mNetworkStatsManager).when(mContext).getSystemService(NetworkStatsManager.class);
+ mThread = new HandlerThread("TetheringMetricsTest");
+ mThread.start();
+ mHandler = new Handler(mThread.getLooper());
+ doReturn(mHandler).when(mDeps).createHandler();
+ // Set up the usage for upstream types.
+ mMockUpstreamUsageBaseline.put(UT_CELLULAR, new DataUsage(100L, 200L));
+ mMockUpstreamUsageBaseline.put(UT_WIFI, new DataUsage(400L, 800L));
+ mMockUpstreamUsageBaseline.put(UT_BLUETOOTH, new DataUsage(50L, 80L));
+ mMockUpstreamUsageBaseline.put(UT_ETHERNET, new DataUsage(0L, 0L));
+ doAnswer(inv -> {
+ final NetworkTemplate template = (NetworkTemplate) inv.getArguments()[0];
+ final DataUsage dataUsage = mMockUpstreamUsageBaseline.getOrDefault(
+ matchRuleToUpstreamType(template.getMatchRule()), new DataUsage(0L, 0L));
+ return makeNetworkStatsWithTxRxBytes(dataUsage);
+ }).when(mNetworkStatsManager).queryDetailsForUidTagState(any(), eq(Long.MIN_VALUE),
+ eq(Long.MAX_VALUE), eq(UID_TETHERING), eq(TAG_NONE), eq(STATE_ALL));
+ mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
mElapsedRealtime = 0L;
}
+ @After
+ public void tearDown() throws Exception {
+ if (mThread != null) {
+ mThread.quitSafely();
+ mThread.join();
+ }
+ }
+
private void verifyReport(final DownstreamType downstream, final ErrorCode error,
final UserType user, final UpstreamEvents.Builder upstreamEvents, final long duration)
throws Exception {
@@ -126,12 +195,18 @@
.setUpstreamEvents(upstreamEvents)
.setDurationMillis(duration)
.build();
- verify(mTetheringMetrics).write(expectedReport);
+ verify(mDeps).write(expectedReport);
+ }
+
+ private void runAndWaitForIdle(Runnable r) {
+ r.run();
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
}
private void updateErrorAndSendReport(final int downstream, final int error) {
mTetheringMetrics.updateErrorCode(downstream, error);
mTetheringMetrics.sendReport(downstream);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
}
private static NetworkCapabilities buildUpstreamCapabilities(final int[] transports) {
@@ -162,7 +237,8 @@
private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
throws Exception {
- mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+ mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
@@ -172,9 +248,7 @@
verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN,
upstreamEvents, getElapsedRealtime());
- reset(mTetheringMetrics);
clearElapsedRealtime();
- mTetheringMetrics.cleanup();
}
@Test
@@ -189,19 +263,19 @@
private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, errorCode);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, duration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
upstreamEvents, getElapsedRealtime());
- reset(mTetheringMetrics);
clearElapsedRealtime();
- mTetheringMetrics.cleanup();
}
@Test
@@ -231,7 +305,8 @@
private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+ mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg));
final long duration = 1 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -241,9 +316,7 @@
addUpstreamEvent(upstreamEvents, UpstreamType.UT_NO_NETWORK, duration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult,
upstreamEvents, getElapsedRealtime());
- reset(mTetheringMetrics);
clearElapsedRealtime();
- mTetheringMetrics.cleanup();
}
@Test
@@ -256,8 +329,9 @@
private void runUpstreamTypesTest(final UpstreamNetworkState ns,
final UpstreamType expectedResult) throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.maybeUpdateUpstreamType(ns);
+ mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+ runAndWaitForIdle(() -> mTetheringMetrics.maybeUpdateUpstreamType(ns));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -266,18 +340,16 @@
addUpstreamEvent(upstreamEvents, expectedResult, duration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_UNKNOWN, upstreamEvents, getElapsedRealtime());
- reset(mTetheringMetrics);
clearElapsedRealtime();
- mTetheringMetrics.cleanup();
}
@Test
public void testUpstreamTypes() throws Exception {
runUpstreamTypesTest(null , UpstreamType.UT_NO_NETWORK);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UpstreamType.UT_CELLULAR);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UpstreamType.UT_WIFI);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UpstreamType.UT_BLUETOOTH);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UpstreamType.UT_ETHERNET);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UT_CELLULAR);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UT_WIFI);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UT_BLUETOOTH);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UT_ETHERNET);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI_AWARE), UpstreamType.UT_WIFI_AWARE);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_LOWPAN), UpstreamType.UT_LOWPAN);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI,
@@ -286,13 +358,13 @@
@Test
public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
final long usbTetheringStartTime = currentTimeMillis();
incrementCurrentTime(2 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG));
final long bluetoothTetheringStartTime = currentTimeMillis();
incrementCurrentTime(3 * SECOND_IN_MILLIS);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
@@ -326,19 +398,20 @@
@Test
public void testUpstreamsWithMultipleDownstreams() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long wifiUpstreamStartTime = currentTimeMillis();
incrementCurrentTime(5 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
final long usbTetheringStartTime = currentTimeMillis();
incrementCurrentTime(5 * SECOND_IN_MILLIS);
updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder usbTetheringUpstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(usbTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+ addUpstreamEvent(usbTetheringUpstreamEvents, UT_WIFI,
currentTimeMillis() - usbTetheringStartTime, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_NO_ERROR,
UserType.USER_SYSTEMUI, usbTetheringUpstreamEvents,
@@ -347,7 +420,7 @@
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder wifiTetheringUpstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(wifiTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+ addUpstreamEvent(wifiTetheringUpstreamEvents, UT_WIFI,
currentTimeMillis() - wifiUpstreamStartTime, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_SETTINGS, wifiTetheringUpstreamEvents,
@@ -356,27 +429,206 @@
@Test
public void testSwitchingMultiUpstreams() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long wifiDuration = 5 * SECOND_IN_MILLIS;
incrementCurrentTime(wifiDuration);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
incrementCurrentTime(bluetoothDuration);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
final long celltoothDuration = 20 * SECOND_IN_MILLIS;
incrementCurrentTime(celltoothDuration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, wifiDuration, 0L, 0L);
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_CELLULAR, celltoothDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_CELLULAR, celltoothDuration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_SETTINGS, upstreamEvents,
currentTimeMillis() - wifiTetheringStartTime);
}
+
+ private void runUsageSupportedForUpstreamTypeTest(final UpstreamType upstreamType,
+ final boolean isSupported) {
+ final boolean result = TetheringMetrics.isUsageSupportedForUpstreamType(upstreamType);
+ assertEquals(isSupported, result);
+ }
+
+ @Test
+ public void testUsageSupportedForUpstreamTypeTest() {
+ runUsageSupportedForUpstreamTypeTest(UT_CELLULAR, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_WIFI, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_BLUETOOTH, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_ETHERNET, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
+ }
+
+ private void runBuildNetworkTemplateForUpstreamType(final UpstreamType upstreamType,
+ final int matchRule) {
+ final NetworkTemplate template =
+ TetheringMetrics.buildNetworkTemplateForUpstreamType(upstreamType);
+ if (matchRule == MATCH_NONE) {
+ assertNull(template);
+ } else {
+ assertEquals(matchRule, template.getMatchRule());
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testBuildNetworkTemplateForUpstreamType() {
+ runBuildNetworkTemplateForUpstreamType(UT_CELLULAR, MATCH_MOBILE);
+ runBuildNetworkTemplateForUpstreamType(UT_WIFI, MATCH_WIFI);
+ runBuildNetworkTemplateForUpstreamType(UT_BLUETOOTH, MATCH_BLUETOOTH);
+ runBuildNetworkTemplateForUpstreamType(UT_ETHERNET, MATCH_ETHERNET);
+ runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
+ runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
+ runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
+ }
+
+ private void verifyEmptyUsageForAllUpstreamTypes() {
+ mHandler.post(() -> {
+ for (UpstreamType type : UpstreamType.values()) {
+ assertEquals(EMPTY, mTetheringMetrics.getLastReportedUsageFromUpstreamType(type));
+ }
+ });
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ }
+
+ @Test
+ public void testInitializeUpstreamDataUsageBeforeT() {
+ // Verify the usage is empty for all upstream types before initialization.
+ verifyEmptyUsageForAllUpstreamTypes();
+
+ // Verify the usage is still empty after initialization if sdk is lower than T.
+ doReturn(false).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+ runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+ verifyEmptyUsageForAllUpstreamTypes();
+ }
+
+ private android.app.usage.NetworkStats makeNetworkStatsWithTxRxBytes(DataUsage dataUsage) {
+ final NetworkStats testAndroidNetStats =
+ new NetworkStats(0L /* elapsedRealtime */, 1 /* initialSize */).addEntry(
+ new NetworkStats.Entry("test", 10001, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, dataUsage.rxBytes,
+ 10, dataUsage.txBytes, 10, 10));
+ return makePublicStatsFromAndroidNetStats(testAndroidNetStats);
+ }
+
+ private static UpstreamType matchRuleToUpstreamType(int matchRule) {
+ switch (matchRule) {
+ case MATCH_MOBILE:
+ return UT_CELLULAR;
+ case MATCH_WIFI:
+ return UT_WIFI;
+ case MATCH_BLUETOOTH:
+ return UT_BLUETOOTH;
+ case MATCH_ETHERNET:
+ return UT_ETHERNET;
+ default:
+ return UpstreamType.UT_UNKNOWN;
+ }
+ }
+
+ private void initializeUpstreamUsageBaseline() {
+ doReturn(true).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+ runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testInitUpstreamUsageBaselineAndCleanup() {
+ // Verify the usage is empty for all upstream types before initialization.
+ verifyEmptyUsageForAllUpstreamTypes();
+
+ // Verify the usage has been initialized
+ initializeUpstreamUsageBaseline();
+
+ mHandler.post(() -> {
+ for (UpstreamType type : UpstreamType.values()) {
+ final DataUsage dataUsage =
+ mTetheringMetrics.getLastReportedUsageFromUpstreamType(type);
+ if (TetheringMetrics.isUsageSupportedForUpstreamType(type)) {
+ assertEquals(mMockUpstreamUsageBaseline.get(type), dataUsage);
+ } else {
+ assertEquals(EMPTY, dataUsage);
+ }
+ }
+ });
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+
+ // Verify the usage is empty after clean up
+ runAndWaitForIdle(() -> mTetheringMetrics.cleanup());
+ verifyEmptyUsageForAllUpstreamTypes();
+ }
+
+ private void updateUpstreamDataUsage(UpstreamType type, long usageDiff) {
+ final DataUsage oldWifiUsage = mMockUpstreamUsageBaseline.get(type);
+ final DataUsage newWifiUsage = new DataUsage(
+ oldWifiUsage.txBytes + usageDiff,
+ oldWifiUsage.rxBytes + usageDiff);
+ mMockUpstreamUsageBaseline.put(type, newWifiUsage);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDataUsageCalculation() throws Exception {
+ initializeUpstreamUsageBaseline();
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
+ final long wifiTetheringStartTime = currentTimeMillis();
+ incrementCurrentTime(1 * SECOND_IN_MILLIS);
+
+ // Change the upstream to Wi-Fi and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+ final long wifiDuration = 5 * SECOND_IN_MILLIS;
+ final long wifiUsageDiff = 100L;
+ incrementCurrentTime(wifiDuration);
+ updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff);
+
+ // Change the upstream to bluetooth and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
+ final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
+ final long btUsageDiff = 50L;
+ incrementCurrentTime(bluetoothDuration);
+ updateUpstreamDataUsage(UT_BLUETOOTH, btUsageDiff);
+
+ // Change the upstream to cellular and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
+ final long cellDuration = 20 * SECOND_IN_MILLIS;
+ final long cellUsageDiff = 500L;
+ incrementCurrentTime(cellDuration);
+ updateUpstreamDataUsage(UT_CELLULAR, cellUsageDiff);
+
+ // Change the upstream back to Wi-FI and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+ final long wifiDuration2 = 50 * SECOND_IN_MILLIS;
+ final long wifiUsageDiff2 = 1000L;
+ incrementCurrentTime(wifiDuration2);
+ updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff2);
+
+ // Stop tethering and verify that the data usage is uploaded.
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+ UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, wifiUsageDiff, wifiUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, btUsageDiff, btUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_CELLULAR, cellDuration, cellUsageDiff, cellUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration2, wifiUsageDiff2, wifiUsageDiff2);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
+ UserType.USER_SETTINGS, upstreamEvents,
+ currentTimeMillis() - wifiTetheringStartTime);
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
index f8e98e3..2417385 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -19,9 +19,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.util.State
+import com.android.net.module.util.SyncStateMachine
+import com.android.net.module.util.SyncStateMachine.StateInfo
import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java
index 94ce2b6..f0770f9 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java
@@ -15,8 +15,6 @@
*/
package com.android.networkstack.tethering.util;
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
-import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.system.OsConstants.AF_UNIX;
import static android.system.OsConstants.EAGAIN;
@@ -25,8 +23,6 @@
import static android.system.OsConstants.SOCK_NONBLOCK;
import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
import android.net.LinkAddress;
import android.net.MacAddress;
@@ -43,9 +39,7 @@
import com.android.net.module.util.structs.EthernetHeader;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.Ipv6Header;
-import com.android.testutils.MiscAsserts;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -61,13 +55,6 @@
private static final LinkAddress TEST_CLIENT_ADDR = new LinkAddress("192.168.43.5/24");
private static final int PACKET_SIZE = 1500;
- private TetheringRequestParcel mTetheringRequest;
-
- @Before
- public void setUp() {
- mTetheringRequest = makeTetheringRequestParcel();
- }
-
public TetheringRequestParcel makeTetheringRequestParcel() {
final TetheringRequestParcel request = new TetheringRequestParcel();
request.tetheringType = TETHERING_WIFI;
@@ -78,40 +65,6 @@
return request;
}
- @Test
- public void testIsTetheringRequestEquals() {
- TetheringRequestParcel request = makeTetheringRequestParcel();
-
- assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, mTetheringRequest));
- assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
- assertTrue(TetheringUtils.isTetheringRequestEquals(null, null));
- assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, null));
- assertFalse(TetheringUtils.isTetheringRequestEquals(null, mTetheringRequest));
-
- request = makeTetheringRequestParcel();
- request.tetheringType = TETHERING_USB;
- assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
- request = makeTetheringRequestParcel();
- request.localIPv4Address = null;
- request.staticClientAddress = null;
- assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
- request = makeTetheringRequestParcel();
- request.exemptFromEntitlementCheck = true;
- assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
- request = makeTetheringRequestParcel();
- request.showProvisioningUi = false;
- assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
- request = makeTetheringRequestParcel();
- request.connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
- assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
- MiscAsserts.assertFieldCountEquals(6, TetheringRequestParcel.class);
- }
-
// Writes the specified packet to a filedescriptor, skipping the Ethernet header.
// Needed because the Ipv6Utils methods for building packets always include the Ethernet header,
// but socket filters applied by TetheringUtils expect the packet to start from the IP header.
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
index b7dc66e..ed4f3da 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
@@ -16,6 +16,8 @@
package com.android.networkstack.tethering.util;
+import static com.android.testutils.HandlerUtils.waitForIdle;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.reset;
@@ -23,7 +25,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
import android.os.UserHandle;
import androidx.test.filters.SmallTest;
@@ -33,7 +35,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -44,9 +45,11 @@
public class VersionedBroadcastListenerTest {
private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
private static final String ACTION_TEST = "action.test.happy.broadcasts";
+ private static final long TEST_TIMEOUT_MS = 10_000L;
@Mock private Context mContext;
private BroadcastInterceptingContext mServiceContext;
+ private HandlerThread mHandlerThread;
private Handler mHandler;
private VersionedBroadcastListener mListener;
private int mCallbackCount;
@@ -61,18 +64,13 @@
}
}
- @BeforeClass
- public static void setUpBeforeClass() throws Exception {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- }
-
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
reset(mContext);
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mServiceContext = new MockContext(mContext);
- mHandler = new Handler(Looper.myLooper());
mCallbackCount = 0;
final IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_TEST);
@@ -85,11 +83,15 @@
mListener.stopListening();
mListener = null;
}
+ mHandlerThread.quitSafely();
+ mHandlerThread.join(TEST_TIMEOUT_MS);
}
private void sendBroadcast() {
final Intent intent = new Intent(ACTION_TEST);
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ // Sending the broadcast is synchronous, but the receiver just posts on the handler
+ waitForIdle(mHandler, TEST_TIMEOUT_MS);
}
@Test
diff --git a/DnsResolver/Android.bp b/bpf/dns_helper/Android.bp
similarity index 100%
rename from DnsResolver/Android.bp
rename to bpf/dns_helper/Android.bp
diff --git a/DnsResolver/DnsBpfHelper.cpp b/bpf/dns_helper/DnsBpfHelper.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelper.cpp
rename to bpf/dns_helper/DnsBpfHelper.cpp
diff --git a/DnsResolver/DnsBpfHelper.h b/bpf/dns_helper/DnsBpfHelper.h
similarity index 100%
rename from DnsResolver/DnsBpfHelper.h
rename to bpf/dns_helper/DnsBpfHelper.h
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/bpf/dns_helper/DnsBpfHelperTest.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelperTest.cpp
rename to bpf/dns_helper/DnsBpfHelperTest.cpp
diff --git a/DnsResolver/DnsHelper.cpp b/bpf/dns_helper/DnsHelper.cpp
similarity index 100%
rename from DnsResolver/DnsHelper.cpp
rename to bpf/dns_helper/DnsHelper.cpp
diff --git a/DnsResolver/include/DnsHelperPublic.h b/bpf/dns_helper/include/DnsHelperPublic.h
similarity index 100%
rename from DnsResolver/include/DnsHelperPublic.h
rename to bpf/dns_helper/include/DnsHelperPublic.h
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
similarity index 100%
rename from DnsResolver/libcom.android.tethering.dns_helper.map.txt
rename to bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
diff --git a/staticlibs/native/bpf_headers/Android.bp b/bpf/headers/Android.bp
similarity index 93%
rename from staticlibs/native/bpf_headers/Android.bp
rename to bpf/headers/Android.bp
index d55584a..aaf8d8d 100644
--- a/staticlibs/native/bpf_headers/Android.bp
+++ b/bpf/headers/Android.bp
@@ -48,11 +48,10 @@
"BpfMapTest.cpp",
"BpfRingbufTest.cpp",
],
- defaults: ["bpf_defaults"],
+ defaults: ["bpf_cc_defaults"],
cflags: [
- "-Wall",
- "-Werror",
- "-Wno-error=unused-variable",
+ "-Wno-unused-variable",
+ "-Wno-sign-compare",
],
header_libs: ["bpf_headers"],
static_libs: ["libgmock"],
diff --git a/staticlibs/native/bpf_headers/BpfMapTest.cpp b/bpf/headers/BpfMapTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_headers/BpfMapTest.cpp
rename to bpf/headers/BpfMapTest.cpp
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/bpf/headers/BpfRingbufTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_headers/BpfRingbufTest.cpp
rename to bpf/headers/BpfRingbufTest.cpp
diff --git a/staticlibs/native/bpf_headers/TEST_MAPPING b/bpf/headers/TEST_MAPPING
similarity index 100%
rename from staticlibs/native/bpf_headers/TEST_MAPPING
rename to bpf/headers/TEST_MAPPING
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
rename to bpf/headers/include/bpf/BpfClassic.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfMap.h
rename to bpf/headers/include/bpf/BpfMap.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/bpf/headers/include/bpf/BpfRingbuf.h
similarity index 95%
rename from staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
rename to bpf/headers/include/bpf/BpfRingbuf.h
index cd51004..4bcd259 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/bpf/headers/include/bpf/BpfRingbuf.h
@@ -20,6 +20,7 @@
#include <android-base/unique_fd.h>
#include <linux/bpf.h>
#include <poll.h>
+#include <sys/epoll.h>
#include <sys/mman.h>
#include <utils/Log.h>
@@ -33,7 +34,7 @@
// BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
class BpfRingbufBase {
public:
- ~BpfRingbufBase() {
+ virtual ~BpfRingbufBase() {
if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
if (mProducerPos) munmap(mProducerPos, mProducerSize);
mConsumerPos = nullptr;
@@ -139,12 +140,24 @@
static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
const char* path);
+ int epoll_ctl_add(int epfd, struct epoll_event *event) {
+ return epoll_ctl(epfd, EPOLL_CTL_ADD, mRingFd.get(), event);
+ }
+
+ int epoll_ctl_mod(int epfd, struct epoll_event *event) {
+ return epoll_ctl(epfd, EPOLL_CTL_MOD, mRingFd.get(), event);
+ }
+
+ int epoll_ctl_del(int epfd) {
+ return epoll_ctl(epfd, EPOLL_CTL_DEL, mRingFd.get(), NULL);
+ }
+
// Consumes all messages from the ring buffer, passing them to the callback.
// Returns the number of messages consumed or a non-ok result on error. If the
// ring buffer has no pending messages an OK result with count 0 is returned.
base::Result<int> ConsumeAll(const MessageCallback& callback);
- private:
+ protected:
// Empty ctor for use by Create.
BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
};
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
rename to bpf/headers/include/bpf/BpfUtils.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
similarity index 97%
rename from staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
rename to bpf/headers/include/bpf/KernelUtils.h
index 417a5c4..68bc607 100644
--- a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,11 +55,12 @@
isKernelVersion(4, 9) || // minimum for Android S & T
isKernelVersion(4, 14) || // minimum for Android U
isKernelVersion(4, 19) || // minimum for Android V
- isKernelVersion(5, 4) || // first supported in Android R
+ isKernelVersion(5, 4) || // first supported in Android R, min for W
isKernelVersion(5, 10) || // first supported in Android S
isKernelVersion(5, 15) || // first supported in Android T
isKernelVersion(6, 1) || // first supported in Android U
- isKernelVersion(6, 6); // first supported in Android V
+ isKernelVersion(6, 6) || // first supported in Android V
+ isKernelVersion(6, 12); // first supported in Android W
}
// Figure out the bitness of userspace.
diff --git a/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h b/bpf/headers/include/bpf/WaitForProgsLoaded.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
rename to bpf/headers/include/bpf/WaitForProgsLoaded.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
similarity index 91%
rename from staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
rename to bpf/headers/include/bpf_helpers.h
index e11a9e2..ac5ffda 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -7,7 +7,7 @@
#include "bpf_map_def.h"
/******************************************************************************
- * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MASTER ARE LIKELY TO BREAK *
+ * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MAIN ARE LIKELY TO BREAK *
* DEVICE COMPATIBILITY WITH MAINLINE MODULES SHIPPING EBPF CODE. *
* *
* THIS WILL LIKELY RESULT IN BRICKED DEVICES AT SOME ARBITRARY FUTURE TIME *
@@ -50,18 +50,21 @@
// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
#define BPFLOADER_MAINLINE_VERSION 42u
-// Android Mainline BpfLoader when running on Android T
+// Android Mainline BpfLoader when running on Android T (sdk=33)
#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android U
+// Android Mainline BpfLoader when running on Android U (sdk=34)
#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
// Android Mainline BpfLoader when running on Android U QPR3
#define BPFLOADER_MAINLINE_U_QPR3_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android V
+// Android Mainline BpfLoader when running on Android V (sdk=35)
#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
+// Android Mainline BpfLoader when running on Android W (sdk=36)
+#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
* process the resulting .o file.
@@ -71,11 +74,11 @@
* In which case it's just best to use the default.
*/
#ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION // inclusive, ie. >=
#endif
#ifndef BPFLOADER_MAX_VER
-#define BPFLOADER_MAX_VER DEFAULT_BPFLOADER_MAX_VER
+#define BPFLOADER_MAX_VER 0x10000u // exclusive, ie. < v1.0
#endif
/* place things in different elf sections */
@@ -99,26 +102,19 @@
*
* If missing, bpfloader_{min/max}_ver default to 0/0x10000 ie. [v0.0, v1.0),
* while size_of_bpf_{map/prog}_def default to 32/20 which are the v0.0 sizes.
- */
-#define LICENSE(NAME) \
- unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER; \
- unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
- size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
- size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
- char _license[] SECTION("license") = (NAME)
-
-/* This macro disables loading BTF map debug information on Android <=U *and* all user builds.
*
- * Note: Bpfloader v0.39+ honours 'btf_user_min_bpfloader_ver' on user builds,
- * and 'btf_min_bpfloader_ver' on non-user builds.
- * Older BTF capable versions unconditionally honour 'btf_min_bpfloader_ver'
+ * This macro also disables loading BTF map debug information, as versions
+ * of the platform bpfloader that support BTF require fork-exec of btfloader
+ * which causes a regression in boot time.
*/
-#define DISABLE_BTF_ON_USER_BUILDS() \
- unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = 39u; \
- unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu
-
-#define DISABLE_ON_MAINLINE_BEFORE_U_QPR3() \
- unsigned _netbpfload_min_ver SECTION("netbpfload_min_ver") = BPFLOADER_MAINLINE_U_QPR3_VERSION;
+#define LICENSE(NAME) \
+ unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER; \
+ unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
+ size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
+ size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
+ unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_VERSION; \
+ unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu; \
+ char _license[] SECTION("license") = (NAME)
/* flag the resulting bpf .o file as critical to system functionality,
* loading all kernel version appropriate programs in it must succeed
@@ -295,6 +291,12 @@
bpf_ringbuf_submit_unsafe(v, 0); \
}
+#define DEFINE_BPF_RINGBUF(the_map, ValueType, size_bytes, usr, grp, md) \
+ DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
/* There exist buggy kernels with pre-T OS, that due to
* kernel patch "[ALPS05162612] bpf: fix ubsan error"
* do not support userspace writes into non-zero index of bpf map arrays.
@@ -353,11 +355,17 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// for maps not meant to be accessed from userspace
+#define DEFINE_BPF_MAP_KERNEL_INTERNAL(the_map, TYPE, KeyType, ValueType, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT, \
+ 0000, "fs_bpf_loader", "", PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -390,13 +398,18 @@
static int (*bpf_probe_read_user_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
-static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
+// GPL only:
+static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
+#define bpf_printf(s, n...) bpf_trace_printk(s, sizeof(s), ## n)
+// Note: bpf only supports up to 3 arguments, log via: bpf_printf("msg %d %d %d", 1, 2, 3);
+// and read via the blocking: sudo cat /sys/kernel/debug/tracing/trace_pipe
+
#define DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
min_loader, max_loader, opt, selinux, pindir, ignore_eng, \
ignore_user, ignore_userdebug) \
@@ -417,19 +430,10 @@
SECTION(SECTION_NAME) \
int the_prog
-#ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
-#define DEFAULT_BPF_PROG_SELINUX_CONTEXT ""
-#endif
-
-#ifndef DEFAULT_BPF_PROG_PIN_SUBDIR
-#define DEFAULT_BPF_PROG_PIN_SUBDIR ""
-#endif
-
#define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
opt) \
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, \
- DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, "", "", \
LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
similarity index 95%
rename from staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
rename to bpf/headers/include/bpf_map_def.h
index 00ef91a..2d6736c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -29,7 +29,7 @@
* *
* ! ! ! W A R N I N G ! ! ! *
* *
- * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MASTER *WILL* BREAK *
+ * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MAIN *WILL* BREAK *
* MAINLINE MODULE COMPATIBILITY *
* *
* AND THUS MAY RESULT IN YOUR DEVICE BRICKING AT SOME ARBITRARY POINT IN *
@@ -42,12 +42,6 @@
* *
******************************************************************************/
-// These are the values used if these fields are missing
-#define DEFAULT_BPFLOADER_MIN_VER 0u // v0.0 (this is inclusive ie. >= v0.0)
-#define DEFAULT_BPFLOADER_MAX_VER 0x10000u // v1.0 (this is exclusive ie. < v1.0)
-#define DEFAULT_SIZEOF_BPF_MAP_DEF 32 // v0.0 struct: enum (uint sized) + 7 uint
-#define DEFAULT_SIZEOF_BPF_PROG_DEF 20 // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-
/*
* The bpf_{map,prog}_def structures are compiled for different architectures.
* Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/netbpfload/Android.bp b/bpf/loader/Android.bp
similarity index 92%
rename from netbpfload/Android.bp
rename to bpf/loader/Android.bp
index 908bb13..b08913a 100644
--- a/netbpfload/Android.bp
+++ b/bpf/loader/Android.bp
@@ -33,12 +33,7 @@
cc_binary {
name: "netbpfload",
- defaults: ["bpf_defaults"],
- cflags: [
- "-Wall",
- "-Werror",
- "-Wthread-safety",
- ],
+ defaults: ["bpf_cc_defaults"],
sanitize: {
integer_overflow: true,
},
@@ -48,10 +43,7 @@
"libbase",
"liblog",
],
- srcs: [
- "loader.cpp",
- "NetBpfLoad.cpp",
- ],
+ srcs: ["NetBpfLoad.cpp"],
apex_available: [
"com.android.tethering",
"//apex_available:platform",
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
new file mode 100644
index 0000000..9a049c7
--- /dev/null
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -0,0 +1,1686 @@
+/*
+ * Copyright (C) 2018-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.
+ */
+
+#define LOG_TAG "NetBpfLoad"
+
+#include <arpa/inet.h>
+#include <dirent.h>
+#include <elf.h>
+#include <errno.h>
+#include <error.h>
+#include <fcntl.h>
+#include <fstream>
+#include <inttypes.h>
+#include <iostream>
+#include <linux/unistd.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <optional>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <unordered_map>
+#include <vector>
+
+#include <android-base/cmsg.h>
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+#include <android-base/properties.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <android/api-level.h>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "bpf_map_def.h"
+
+// The following matches bpf_helpers.h, which is only for inclusion in bpf code
+#define BPFLOADER_MAINLINE_VERSION 42u
+
+using android::base::EndsWith;
+using android::base::GetIntProperty;
+using android::base::GetProperty;
+using android::base::InitLogging;
+using android::base::KernelLogger;
+using android::base::SetProperty;
+using android::base::Split;
+using android::base::StartsWith;
+using android::base::Tokenize;
+using android::base::unique_fd;
+using std::ifstream;
+using std::ios;
+using std::optional;
+using std::string;
+using std::vector;
+
+namespace android {
+namespace bpf {
+
+// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
+//
+// The BpfLoader needs to convert these bpf.o specified strings into an enum
+// for internal use (to check that valid values were specified for the specific
+// location of the bpf.o file).
+//
+// It also needs to map selinux_context's into pin_subdir's.
+// This is because of how selinux_context is actually implemented via pin+rename.
+//
+// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
+// is aware of. Thus there currently needs to be a 1:1 mapping between the two.
+//
+enum class domain : int {
+ unspecified = 0, // means just use the default for that specific pin location
+ tethering, // (S+) fs_bpf_tethering /sys/fs/bpf/tethering
+ net_private, // (T+) fs_bpf_net_private /sys/fs/bpf/net_private
+ net_shared, // (T+) fs_bpf_net_shared /sys/fs/bpf/net_shared
+ netd_readonly, // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
+ netd_shared, // (T+) fs_bpf_netd_shared /sys/fs/bpf/netd_shared
+ loader, // (U+) fs_bpf_loader /sys/fs/bpf/loader
+ // on T due to lack of sepolicy/genfscon rules it behaves simply as 'fs_bpf'
+};
+
+static constexpr domain AllDomains[] = {
+ domain::unspecified,
+ domain::tethering,
+ domain::net_private,
+ domain::net_shared,
+ domain::netd_readonly,
+ domain::netd_shared,
+ domain::loader,
+};
+
+static constexpr bool specified(domain d) {
+ return d != domain::unspecified;
+}
+
+struct Location {
+ const char* const dir = "";
+ const char* const prefix = "";
+};
+
+// Returns the build type string (from ro.build.type).
+const std::string& getBuildType() {
+ static std::string t = GetProperty("ro.build.type", "unknown");
+ return t;
+}
+
+// The following functions classify the 3 Android build types.
+inline bool isEng() {
+ return getBuildType() == "eng";
+}
+
+inline bool isUser() {
+ return getBuildType() == "user";
+}
+
+inline bool isUserdebug() {
+ return getBuildType() == "userdebug";
+}
+
+#define BPF_FS_PATH "/sys/fs/bpf/"
+
+static unsigned int page_size = static_cast<unsigned int>(getpagesize());
+
+constexpr const char* lookupSelinuxContext(const domain d) {
+ switch (d) {
+ case domain::unspecified: return "";
+ case domain::tethering: return "fs_bpf_tethering";
+ case domain::net_private: return "fs_bpf_net_private";
+ case domain::net_shared: return "fs_bpf_net_shared";
+ case domain::netd_readonly: return "fs_bpf_netd_readonly";
+ case domain::netd_shared: return "fs_bpf_netd_shared";
+ case domain::loader: return "fs_bpf_loader";
+ }
+}
+
+domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
+ for (domain d : AllDomains) {
+ // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+ if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
+ if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
+ }
+ ALOGE("unrecognized selinux_context '%-32s'", s);
+ // Note: we *can* just abort() here as we only load bpf .o files shipped
+ // in the same mainline module / apex as NetBpfLoad itself.
+ abort();
+}
+
+constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
+ switch (d) {
+ case domain::unspecified: return unspecified;
+ case domain::tethering: return "tethering/";
+ case domain::net_private: return "net_private/";
+ case domain::net_shared: return "net_shared/";
+ case domain::netd_readonly: return "netd_readonly/";
+ case domain::netd_shared: return "netd_shared/";
+ case domain::loader: return "loader/";
+ }
+};
+
+domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
+ for (domain d : AllDomains) {
+ // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+ if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
+ if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
+ }
+ ALOGE("unrecognized pin_subdir '%-32s'", s);
+ // Note: we *can* just abort() here as we only load bpf .o files shipped
+ // in the same mainline module / apex as NetBpfLoad itself.
+ abort();
+}
+
+static string pathToObjName(const string& path) {
+ // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
+ string filename = Split(path, "/").back();
+ // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
+ string name = filename.substr(0, filename.find_last_of('.'));
+ // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
+ // this can be used to provide duplicate programs (mux based on the bpfloader version)
+ return name.substr(0, name.find_last_of('@'));
+}
+
+typedef struct {
+ const char* name;
+ enum bpf_prog_type type;
+ enum bpf_attach_type attach_type;
+} sectionType;
+
+/*
+ * Map section name prefixes to program types, the section name will be:
+ * SECTION(<prefix>/<name-of-program>)
+ * For example:
+ * SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
+ * is the name of the program, and tracepoint is the type.
+ *
+ * However, be aware that you should not be directly using the SECTION() macro.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
+ */
+sectionType sectionNameTypes[] = {
+ {"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
+ {"bind6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
+ {"cgroupskb/", BPF_PROG_TYPE_CGROUP_SKB},
+ {"cgroupsock/", BPF_PROG_TYPE_CGROUP_SOCK},
+ {"cgroupsockcreate/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_CREATE},
+ {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_RELEASE},
+ {"connect4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
+ {"connect6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
+ {"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
+ {"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
+ {"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
+ {"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
+ {"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
+ {"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
+ {"recvmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
+ {"schedact/", BPF_PROG_TYPE_SCHED_ACT},
+ {"schedcls/", BPF_PROG_TYPE_SCHED_CLS},
+ {"sendmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
+ {"sendmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
+ {"setsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT},
+ {"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER},
+ {"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
+ {"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
+ {"xdp/", BPF_PROG_TYPE_XDP},
+};
+
+typedef struct {
+ enum bpf_prog_type type;
+ enum bpf_attach_type attach_type;
+ string name;
+ vector<char> data;
+ vector<char> rel_data;
+ optional<struct bpf_prog_def> prog_def;
+
+ unique_fd prog_fd; // fd after loading
+} codeSection;
+
+static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
+ elfFile.seekg(0);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
+
+ return 0;
+}
+
+// Reads all section header tables into an Shdr array
+static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
+ Elf64_Ehdr eh;
+ int ret = 0;
+
+ ret = readElfHeader(elfFile, &eh);
+ if (ret) return ret;
+
+ elfFile.seekg(eh.e_shoff);
+ if (elfFile.fail()) return -1;
+
+ // Read shdr table entries
+ shTable.resize(eh.e_shnum);
+
+ if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
+
+ return 0;
+}
+
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
+ vector<Elf64_Shdr> shTable;
+ int ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ elfFile.seekg(shTable[id].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ sec.resize(shTable[id].sh_size);
+ if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
+
+ return 0;
+}
+
+// Read whole section header string table
+static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
+ Elf64_Ehdr eh;
+ int ret = readElfHeader(elfFile, &eh);
+ if (ret) return ret;
+
+ ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
+ if (ret) return ret;
+
+ return 0;
+}
+
+// Get name from offset in strtab
+static int getSymName(ifstream& elfFile, int nameOff, string& name) {
+ int ret;
+ vector<char> secStrTab;
+
+ ret = readSectionHeaderStrtab(elfFile, secStrTab);
+ if (ret) return ret;
+
+ if (nameOff >= (int)secStrTab.size()) return -1;
+
+ name = string((char*)secStrTab.data() + nameOff);
+ return 0;
+}
+
+// Reads a full section by name - example to get the GPL license
+static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
+ vector<char> secStrTab;
+ vector<Elf64_Shdr> shTable;
+ int ret;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ ret = readSectionHeaderStrtab(elfFile, secStrTab);
+ if (ret) return ret;
+
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ char* secname = secStrTab.data() + shTable[i].sh_name;
+ if (!secname) continue;
+
+ if (!strcmp(secname, name)) {
+ vector<char> dataTmp;
+ dataTmp.resize(shTable[i].sh_size);
+
+ elfFile.seekg(shTable[i].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+ data = dataTmp;
+ return 0;
+ }
+ }
+ return -2;
+}
+
+unsigned int readSectionUint(const char* name, ifstream& elfFile) {
+ vector<char> theBytes;
+ int ret = readSectionByName(name, elfFile, theBytes);
+ if (ret) {
+ ALOGE("Couldn't find section %s.", name);
+ abort();
+ } else if (theBytes.size() < sizeof(unsigned int)) {
+ ALOGE("Section %s is too short.", name);
+ abort();
+ } else {
+ // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
+ unsigned int value = static_cast<unsigned char>(theBytes[3]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[2]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[1]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[0]);
+ ALOGD("Section %s value is %u [0x%x]", name, value, value);
+ return value;
+ }
+}
+
+static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
+ int ret;
+ vector<Elf64_Shdr> shTable;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ if ((int)shTable[i].sh_type != type) continue;
+
+ vector<char> dataTmp;
+ dataTmp.resize(shTable[i].sh_size);
+
+ elfFile.seekg(shTable[i].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+ data = dataTmp;
+ return 0;
+ }
+ return -2;
+}
+
+static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
+ return (a.st_value < b.st_value);
+}
+
+static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
+ int ret, numElems;
+ Elf64_Sym* buf;
+ vector<char> secData;
+
+ ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
+ if (ret) return ret;
+
+ buf = (Elf64_Sym*)secData.data();
+ numElems = (secData.size() / sizeof(Elf64_Sym));
+ data.assign(buf, buf + numElems);
+
+ if (sort) std::sort(data.begin(), data.end(), symCompare);
+ return 0;
+}
+
+static enum bpf_prog_type getSectionType(string& name) {
+ for (auto& snt : sectionNameTypes)
+ if (StartsWith(name, snt.name)) return snt.type;
+
+ return BPF_PROG_TYPE_UNSPEC;
+}
+
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd) {
+ vector<char> pdData;
+ int ret = readSectionByName("progs", elfFile, pdData);
+ if (ret) return ret;
+
+ if (pdData.size() % sizeof(struct bpf_prog_def)) {
+ ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
+ pdData.size(), sizeof(struct bpf_prog_def));
+ return -1;
+ };
+
+ pd.resize(pdData.size() / sizeof(struct bpf_prog_def));
+
+ const char* dataPtr = pdData.data();
+ for (auto& p : pd) {
+ // Copy the structure from the ELF file and move to the next one.
+ memcpy(&p, dataPtr, sizeof(struct bpf_prog_def));
+ dataPtr += sizeof(struct bpf_prog_def);
+ }
+ return 0;
+}
+
+static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
+ optional<unsigned> symbolType = std::nullopt) {
+ int ret;
+ string name;
+ vector<Elf64_Sym> symtab;
+ vector<Elf64_Shdr> shTable;
+
+ ret = readSymTab(elfFile, 1 /* sort */, symtab);
+ if (ret) return ret;
+
+ // Get index of section
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ int sec_idx = -1;
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ ret = getSymName(elfFile, shTable[i].sh_name, name);
+ if (ret) return ret;
+
+ if (!name.compare(sectionName)) {
+ sec_idx = i;
+ break;
+ }
+ }
+
+ // No section found with matching name
+ if (sec_idx == -1) {
+ ALOGW("No %s section could be found in elf object", sectionName.c_str());
+ return -1;
+ }
+
+ for (int i = 0; i < (int)symtab.size(); i++) {
+ if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
+
+ if (symtab[i].st_shndx == sec_idx) {
+ string s;
+ ret = getSymName(elfFile, symtab[i].st_name, s);
+ if (ret) return ret;
+ names.push_back(s);
+ }
+ }
+
+ return 0;
+}
+
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs) {
+ vector<Elf64_Shdr> shTable;
+ int entries, ret = 0;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+ entries = shTable.size();
+
+ vector<struct bpf_prog_def> pd;
+ ret = readProgDefs(elfFile, pd);
+ if (ret) return ret;
+ vector<string> progDefNames;
+ ret = getSectionSymNames(elfFile, "progs", progDefNames);
+ if (!pd.empty() && ret) return ret;
+
+ for (int i = 0; i < entries; i++) {
+ string name;
+ codeSection cs_temp;
+ cs_temp.type = BPF_PROG_TYPE_UNSPEC;
+
+ ret = getSymName(elfFile, shTable[i].sh_name, name);
+ if (ret) return ret;
+
+ enum bpf_prog_type ptype = getSectionType(name);
+
+ if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
+
+ // This must be done before '/' is replaced with '_'.
+ for (auto& snt : sectionNameTypes)
+ if (StartsWith(name, snt.name)) cs_temp.attach_type = snt.attach_type;
+
+ string oldName = name;
+
+ // convert all slashes to underscores
+ std::replace(name.begin(), name.end(), '/', '_');
+
+ cs_temp.type = ptype;
+ cs_temp.name = name;
+
+ ret = readSectionByIdx(elfFile, i, cs_temp.data);
+ if (ret) return ret;
+ ALOGV("Loaded code section %d (%s)", i, name.c_str());
+
+ vector<string> csSymNames;
+ ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
+ if (ret || !csSymNames.size()) return ret;
+ for (size_t i = 0; i < progDefNames.size(); ++i) {
+ if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
+ cs_temp.prog_def = pd[i];
+ break;
+ }
+ }
+
+ // Check for rel section
+ if (cs_temp.data.size() > 0 && i < entries) {
+ ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
+ if (ret) return ret;
+
+ if (name == (".rel" + oldName)) {
+ ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
+ if (ret) return ret;
+ ALOGV("Loaded relo section %d (%s)", i, name.c_str());
+ }
+ }
+
+ if (cs_temp.data.size() > 0) {
+ cs.push_back(std::move(cs_temp));
+ ALOGV("Adding section %d to cs list", i);
+ }
+ }
+ return 0;
+}
+
+static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
+ vector<Elf64_Sym> symtab;
+ int ret = 0;
+
+ ret = readSymTab(elfFile, 0 /* !sort */, symtab);
+ if (ret) return ret;
+
+ if (index >= (int)symtab.size()) return -1;
+
+ return getSymName(elfFile, symtab[index].st_name, name);
+}
+
+static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
+ const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+ // bpfGetFd... family of functions require at minimum a 4.14 kernel,
+ // so on 4.9-T kernels just pretend the map matches our expectations.
+ // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
+ // This is because the primary failure mode we're trying to detect here
+ // is either a source code misconfiguration (which is likely kernel independent)
+ // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
+ if (!isAtLeastKernelVersion(4, 14, 0)) return true;
+
+ // Assuming fd is a valid Bpf Map file descriptor then
+ // all the following should always succeed on a 4.14+ kernel.
+ // If they somehow do fail, they'll return -1 (and set errno),
+ // which should then cause (among others) a key_size mismatch.
+ int fd_type = bpfGetFdMapType(fd);
+ int fd_key_size = bpfGetFdKeySize(fd);
+ int fd_value_size = bpfGetFdValueSize(fd);
+ int fd_max_entries = bpfGetFdMaxEntries(fd);
+ int fd_map_flags = bpfGetFdMapFlags(fd);
+
+ // DEVMAPs are readonly from the bpf program side's point of view, as such
+ // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
+ int desired_map_flags = (int)mapDef.map_flags;
+ if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
+ desired_map_flags |= BPF_F_RDONLY_PROG;
+
+ if (type == BPF_MAP_TYPE_LPM_TRIE)
+ desired_map_flags |= BPF_F_NO_PREALLOC;
+
+ // The .h file enforces that this is a power of two, and page size will
+ // also always be a power of two, so this logic is actually enough to
+ // force it to be a multiple of the page size, as required by the kernel.
+ unsigned int desired_max_entries = mapDef.max_entries;
+ if (type == BPF_MAP_TYPE_RINGBUF) {
+ if (desired_max_entries < page_size) desired_max_entries = page_size;
+ }
+
+ // The following checks should *never* trigger, if one of them somehow does,
+ // it probably means a bpf .o file has been changed/replaced at runtime
+ // and bpfloader was manually rerun (normally it should only run *once*
+ // early during the boot process).
+ // Another possibility is that something is misconfigured in the code:
+ // most likely a shared map is declared twice differently.
+ // But such a change should never be checked into the source tree...
+ if ((fd_type == type) &&
+ (fd_key_size == (int)mapDef.key_size) &&
+ (fd_value_size == (int)mapDef.value_size) &&
+ (fd_max_entries == (int)desired_max_entries) &&
+ (fd_map_flags == desired_map_flags)) {
+ return true;
+ }
+
+ ALOGE("bpf map name %s mismatch: desired/found: "
+ "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
+ mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
+ fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
+ return false;
+}
+
+static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
+ const char* prefix, const unsigned int bpfloader_ver) {
+ int ret;
+ vector<char> mdData;
+ vector<struct bpf_map_def> md;
+ vector<string> mapNames;
+ string objName = pathToObjName(string(elfPath));
+
+ ret = readSectionByName("maps", elfFile, mdData);
+ if (ret == -2) return 0; // no maps to read
+ if (ret) return ret;
+
+ if (mdData.size() % sizeof(struct bpf_map_def)) {
+ ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
+ mdData.size(), sizeof(struct bpf_map_def));
+ return -1;
+ };
+
+ md.resize(mdData.size() / sizeof(struct bpf_map_def));
+
+ const char* dataPtr = mdData.data();
+ for (auto& m : md) {
+ // Copy the structure from the ELF file and move to the next one.
+ memcpy(&m, dataPtr, sizeof(struct bpf_map_def));
+ dataPtr += sizeof(struct bpf_map_def);
+ }
+
+ ret = getSectionSymNames(elfFile, "maps", mapNames);
+ if (ret) return ret;
+
+ unsigned kvers = kernelVersion();
+
+ for (int i = 0; i < (int)mapNames.size(); i++) {
+ if (md[i].zero != 0) abort();
+
+ if (bpfloader_ver < md[i].bpfloader_min_ver) {
+ ALOGD("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+ md[i].bpfloader_min_ver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (bpfloader_ver >= md[i].bpfloader_max_ver) {
+ ALOGD("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+ md[i].bpfloader_max_ver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (kvers < md[i].min_kver) {
+ ALOGD("skipping map %s which requires kernel version 0x%x >= 0x%x",
+ mapNames[i].c_str(), kvers, md[i].min_kver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (kvers >= md[i].max_kver) {
+ ALOGD("skipping map %s which requires kernel version 0x%x < 0x%x",
+ mapNames[i].c_str(), kvers, md[i].max_kver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
+ (md[i].ignore_on_userdebug && isUserdebug())) {
+ ALOGD("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+ getBuildType().c_str());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
+ (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
+ (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
+ (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
+ (isRiscV() && md[i].ignore_on_riscv64)) {
+ ALOGD("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+ describeArch());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
+ // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
+ // of be approximated: ARRAY has the same userspace api, though it is not usable
+ // by the same ebpf programs. However, that's okay because the bpf_redirect_map()
+ // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
+ // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
+ // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
+ // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
+ type = BPF_MAP_TYPE_ARRAY;
+ }
+ if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
+ // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
+ // of be approximated: HASH has the same userspace visible api.
+ // However it cannot be used by ebpf programs in the same way.
+ // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
+ // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
+ // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
+ // programs as being 5.4+...
+ type = BPF_MAP_TYPE_HASH;
+ }
+
+ // The .h file enforces that this is a power of two, and page size will
+ // also always be a power of two, so this logic is actually enough to
+ // force it to be a multiple of the page size, as required by the kernel.
+ unsigned int max_entries = md[i].max_entries;
+ if (type == BPF_MAP_TYPE_RINGBUF) {
+ if (max_entries < page_size) max_entries = page_size;
+ }
+
+ domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
+ if (specified(selinux_context)) {
+ ALOGV("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+ md[i].selinux_context, static_cast<int>(selinux_context),
+ lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+ }
+
+ domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
+ if (specified(pin_subdir)) {
+ ALOGV("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+ static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
+ }
+
+ // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
+ // except that maps shared across .o's have empty <objName>
+ // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
+ string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
+ (md[i].shared ? "" : objName) + "_" + mapNames[i];
+ bool reuse = false;
+ unique_fd fd;
+ int saved_errno;
+
+ if (access(mapPinLoc.c_str(), F_OK) == 0) {
+ fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
+ saved_errno = errno;
+ ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
+ reuse = true;
+ } else {
+ union bpf_attr req = {
+ .map_type = type,
+ .key_size = md[i].key_size,
+ .value_size = md[i].value_size,
+ .max_entries = max_entries,
+ .map_flags = md[i].map_flags | (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0),
+ };
+ if (isAtLeastKernelVersion(4, 15, 0))
+ strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+ fd.reset(bpf(BPF_MAP_CREATE, req));
+ saved_errno = errno;
+ if (fd.ok()) {
+ ALOGD("bpf_create_map[%s] -> %d", mapNames[i].c_str(), fd.get());
+ } else {
+ ALOGE("bpf_create_map[%s] -> %d errno:%d", mapNames[i].c_str(), fd.get(), saved_errno);
+ }
+ }
+
+ if (!fd.ok()) return -saved_errno;
+
+ // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
+ // safety (since reuse code path is rare) run these checks even if we just created it.
+ // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
+ if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
+
+ if (!reuse) {
+ if (specified(selinux_context)) {
+ string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+ "tmp_map_" + objName + "_" + mapNames[i];
+ ret = bpfFdPin(fd, createLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ ret = renameat2(AT_FDCWD, createLoc.c_str(),
+ AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
+ if (ret) {
+ int err = errno;
+ ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
+ err, strerror(err));
+ return -err;
+ }
+ } else {
+ ret = bpfFdPin(fd, mapPinLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ }
+ ret = chmod(mapPinLoc.c_str(), md[i].mode);
+ if (ret) {
+ int err = errno;
+ ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
+ strerror(err));
+ return -err;
+ }
+ ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
+ if (ret) {
+ int err = errno;
+ ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
+ ret, err, strerror(err));
+ return -err;
+ }
+ }
+
+ int mapId = bpfGetFdMapId(fd);
+ if (mapId == -1) {
+ if (isAtLeastKernelVersion(4, 14, 0))
+ ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+ } else {
+ ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
+ }
+
+ mapFds.push_back(std::move(fd));
+ }
+
+ return ret;
+}
+
+static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
+ int insnIndex;
+ struct bpf_insn *insn, *insns;
+
+ insns = (struct bpf_insn*)(insnsPtr);
+
+ insnIndex = offset / sizeof(struct bpf_insn);
+ insn = &insns[insnIndex];
+
+ // Occasionally might be useful for relocation debugging, but pretty spammy
+ if (0) {
+ ALOGV("applying relo to instruction at byte offset: %llu, "
+ "insn offset %d, insn %llx",
+ (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
+ }
+
+ if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
+ ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
+ return;
+ }
+
+ insn->imm = fd;
+ insn->src_reg = BPF_PSEUDO_MAP_FD;
+}
+
+static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
+ vector<string> mapNames;
+
+ int ret = getSectionSymNames(elfFile, "maps", mapNames);
+ if (ret) return;
+
+ for (int k = 0; k != (int)cs.size(); k++) {
+ Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
+ int n_rel = cs[k].rel_data.size() / sizeof(*rel);
+
+ for (int i = 0; i < n_rel; i++) {
+ int symIndex = ELF64_R_SYM(rel[i].r_info);
+ string symName;
+
+ ret = getSymNameByIdx(elfFile, symIndex, symName);
+ if (ret) return;
+
+ // Find the map fd and apply relo
+ for (int j = 0; j < (int)mapNames.size(); j++) {
+ if (!mapNames[j].compare(symName)) {
+ applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
+ break;
+ }
+ }
+ }
+ }
+}
+
+static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
+ const char* prefix, const unsigned int bpfloader_ver) {
+ unsigned kvers = kernelVersion();
+
+ if (!kvers) {
+ ALOGE("unable to get kernel version");
+ return -EINVAL;
+ }
+
+ string objName = pathToObjName(string(elfPath));
+
+ for (int i = 0; i < (int)cs.size(); i++) {
+ unique_fd& fd = cs[i].prog_fd;
+ int ret;
+ string name = cs[i].name;
+
+ if (!cs[i].prog_def.has_value()) {
+ ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
+ return -EINVAL;
+ }
+
+ unsigned min_kver = cs[i].prog_def->min_kver;
+ unsigned max_kver = cs[i].prog_def->max_kver;
+ ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
+ max_kver, kvers);
+ if (kvers < min_kver) continue;
+ if (kvers >= max_kver) continue;
+
+ unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
+ unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
+ domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
+ domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
+
+ ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
+ bpfMinVer, bpfMaxVer);
+ if (bpfloader_ver < bpfMinVer) continue;
+ if (bpfloader_ver >= bpfMaxVer) continue;
+
+ if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
+ (cs[i].prog_def->ignore_on_user && isUser()) ||
+ (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
+ ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
+ getBuildType().c_str());
+ continue;
+ }
+
+ if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
+ (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
+ (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
+ (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
+ (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
+ ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
+ continue;
+ }
+
+ if (specified(selinux_context)) {
+ ALOGV("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+ cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
+ lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+ }
+
+ if (specified(pin_subdir)) {
+ ALOGV("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+ cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
+ lookupPinSubdir(pin_subdir));
+ }
+
+ // strip any potential $foo suffix
+ // this can be used to provide duplicate programs
+ // conditionally loaded based on running kernel version
+ name = name.substr(0, name.find_last_of('$'));
+
+ bool reuse = false;
+ // Format of pin location is
+ // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
+ string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
+ objName + '_' + string(name);
+ if (access(progPinLoc.c_str(), F_OK) == 0) {
+ fd.reset(retrieveProgram(progPinLoc.c_str()));
+ ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
+ (!fd.ok() ? std::strerror(errno) : "no error"));
+ reuse = true;
+ } else {
+ static char log_buf[1 << 20]; // 1 MiB logging buffer
+
+ union bpf_attr req = {
+ .prog_type = cs[i].type,
+ .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+ .insns = ptr_to_u64(cs[i].data.data()),
+ .license = ptr_to_u64(license.c_str()),
+ .log_level = 1,
+ .log_size = sizeof(log_buf),
+ .log_buf = ptr_to_u64(log_buf),
+ .kern_version = kvers,
+ .expected_attach_type = cs[i].attach_type,
+ };
+ if (isAtLeastKernelVersion(4, 15, 0))
+ strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+ fd.reset(bpf(BPF_PROG_LOAD, req));
+
+ // Kernel should have NULL terminated the log buffer, but force it anyway for safety
+ log_buf[sizeof(log_buf) - 1] = 0;
+
+ // Strip out final newline if present
+ int log_chars = strlen(log_buf);
+ if (log_chars && log_buf[log_chars - 1] == '\n') log_buf[--log_chars] = 0;
+
+ bool log_oneline = !strchr(log_buf, '\n');
+
+ ALOGD("BPF_PROG_LOAD call for %s (%s) returned '%s' fd: %d (%s)", elfPath,
+ cs[i].name.c_str(), log_oneline ? log_buf : "{multiline}",
+ fd.get(), (!fd.ok() ? std::strerror(errno) : "ok"));
+
+ if (!fd.ok()) {
+ // kernel NULL terminates log_buf, so this checks for non-empty string
+ if (log_buf[0]) {
+ vector<string> lines = Split(log_buf, "\n");
+
+ ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+ for (const auto& line : lines) ALOGW("%s", line.c_str());
+ ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+ }
+
+ if (cs[i].prog_def->optional) {
+ ALOGW("failed program %s is marked optional - continuing...",
+ cs[i].name.c_str());
+ continue;
+ }
+ ALOGE("non-optional program %s failed to load.", cs[i].name.c_str());
+ }
+ }
+
+ if (!fd.ok()) return fd.get();
+
+ if (!reuse) {
+ if (specified(selinux_context)) {
+ string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+ "tmp_prog_" + objName + '_' + string(name);
+ ret = bpfFdPin(fd, createLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ ret = renameat2(AT_FDCWD, createLoc.c_str(),
+ AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
+ if (ret) {
+ int err = errno;
+ ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
+ err, strerror(err));
+ return -err;
+ }
+ } else {
+ ret = bpfFdPin(fd, progPinLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ }
+ if (chmod(progPinLoc.c_str(), 0440)) {
+ int err = errno;
+ ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
+ return -err;
+ }
+ if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
+ (gid_t)cs[i].prog_def->gid)) {
+ int err = errno;
+ ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
+ cs[i].prog_def->gid, err, strerror(err));
+ return -err;
+ }
+ }
+
+ int progId = bpfGetFdProgId(fd);
+ if (progId == -1) {
+ ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
+ } else {
+ ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+ }
+ }
+
+ return 0;
+}
+
+int loadProg(const char* const elfPath, const unsigned int bpfloader_ver,
+ const char* const prefix) {
+ vector<char> license;
+ vector<codeSection> cs;
+ vector<unique_fd> mapFds;
+ int ret;
+
+ ifstream elfFile(elfPath, ios::in | ios::binary);
+ if (!elfFile.is_open()) return -1;
+
+ ret = readSectionByName("license", elfFile, license);
+ if (ret) {
+ ALOGE("Couldn't find license in %s", elfPath);
+ return ret;
+ } else {
+ ALOGD("Loading ELF object %s with license %s",
+ elfPath, (char*)license.data());
+ }
+
+ unsigned int bpfLoaderMinVer = readSectionUint("bpfloader_min_ver", elfFile);
+ unsigned int bpfLoaderMaxVer = readSectionUint("bpfloader_max_ver", elfFile);
+
+ // inclusive lower bound check
+ if (bpfloader_ver < bpfLoaderMinVer) {
+ ALOGD("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+ bpfloader_ver, elfPath, bpfLoaderMinVer);
+ return 0;
+ }
+
+ // exclusive upper bound check
+ if (bpfloader_ver >= bpfLoaderMaxVer) {
+ ALOGD("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+ bpfloader_ver, elfPath, bpfLoaderMaxVer);
+ return 0;
+ }
+
+ ALOGD("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+ bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+
+ ret = createMaps(elfPath, elfFile, mapFds, prefix, bpfloader_ver);
+ if (ret) {
+ ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
+ return ret;
+ }
+
+ for (int i = 0; i < (int)mapFds.size(); i++)
+ ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+
+ ret = readCodeSections(elfFile, cs);
+ // BPF .o's with no programs are only supported by mainline netbpfload,
+ // make sure .o's targeting non-mainline (ie. S) bpfloader don't show up.
+ if (ret == -ENOENT && bpfLoaderMinVer >= BPFLOADER_MAINLINE_VERSION)
+ return 0;
+ if (ret) {
+ ALOGE("Couldn't read all code sections in %s", elfPath);
+ return ret;
+ }
+
+ applyMapRelo(elfFile, mapFds, cs);
+
+ ret = loadCodeSections(elfPath, cs, string(license.data()), prefix, bpfloader_ver);
+ if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
+
+ return ret;
+}
+
+static bool exists(const char* const path) {
+ int v = access(path, F_OK);
+ if (!v) return true;
+ if (errno == ENOENT) return false;
+ ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
+ abort(); // can only hit this if permissions (likely selinux) are screwed up
+}
+
+#define APEXROOT "/apex/com.android.tethering"
+#define BPFROOT APEXROOT "/etc/bpf"
+
+const Location locations[] = {
+ // S+ Tethering mainline module (network_stack): tether offload
+ {
+ .dir = BPFROOT "/",
+ .prefix = "tethering/",
+ },
+ // T+ Tethering mainline module (shared with netd & system server)
+ // netutils_wrapper (for iptables xt_bpf) has access to programs
+ {
+ .dir = BPFROOT "/netd_shared/",
+ .prefix = "netd_shared/",
+ },
+ // T+ Tethering mainline module (shared with netd & system server)
+ // netutils_wrapper has no access, netd has read only access
+ {
+ .dir = BPFROOT "/netd_readonly/",
+ .prefix = "netd_readonly/",
+ },
+ // T+ Tethering mainline module (shared with system server)
+ {
+ .dir = BPFROOT "/net_shared/",
+ .prefix = "net_shared/",
+ },
+ // T+ Tethering mainline module (not shared, just network_stack)
+ {
+ .dir = BPFROOT "/net_private/",
+ .prefix = "net_private/",
+ },
+};
+
+static int loadAllElfObjects(const unsigned int bpfloader_ver, const Location& location) {
+ int retVal = 0;
+ DIR* dir;
+ struct dirent* ent;
+
+ if ((dir = opendir(location.dir)) != NULL) {
+ while ((ent = readdir(dir)) != NULL) {
+ string s = ent->d_name;
+ if (!EndsWith(s, ".o")) continue;
+
+ string progPath(location.dir);
+ progPath += s;
+
+ int ret = loadProg(progPath.c_str(), bpfloader_ver, location.prefix);
+ if (ret) {
+ retVal = ret;
+ ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
+ } else {
+ ALOGD("Loaded object: %s", progPath.c_str());
+ }
+ }
+ closedir(dir);
+ }
+ return retVal;
+}
+
+static int createSysFsBpfSubDir(const char* const prefix) {
+ if (*prefix) {
+ mode_t prevUmask = umask(0);
+
+ string s = "/sys/fs/bpf/";
+ s += prefix;
+
+ errno = 0;
+ int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
+ if (ret && errno != EEXIST) {
+ const int err = errno;
+ ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
+ return -err;
+ }
+
+ umask(prevUmask);
+ }
+ return 0;
+}
+
+// Technically 'value' doesn't need to be newline terminated, but it's best
+// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
+// which is usually how kernel devs test the actual sysctl interfaces.
+static int writeProcSysFile(const char *filename, const char *value) {
+ unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+ if (fd < 0) {
+ const int err = errno;
+ ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
+ return -err;
+ }
+ int len = strlen(value);
+ int v = write(fd, value, len);
+ if (v < 0) {
+ const int err = errno;
+ ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
+ return -err;
+ }
+ if (v != len) {
+ // In practice, due to us only using this for /proc/sys/... files, this can't happen.
+ ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+#define APEX_MOUNT_POINT "/apex/com.android.tethering"
+const char * const platformBpfLoader = "/system/bin/bpfloader";
+
+static 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;
+ ALOGV("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;
+}
+
+static bool hasGSM() {
+ static string ph = GetProperty("gsm.current.phone-type", "");
+ static bool gsm = (ph != "");
+ static bool logged = false;
+ if (!logged) {
+ logged = true;
+ ALOGI("hasGSM(gsm.current.phone-type='%s'): %s", ph.c_str(), gsm ? "true" : "false");
+ }
+ return gsm;
+}
+
+static bool isTV() {
+ if (hasGSM()) return false; // TVs don't do GSM
+
+ static string key = GetProperty("ro.oem.key1", "");
+ static bool tv = StartsWith(key, "ATV00");
+ static bool logged = false;
+ if (!logged) {
+ logged = true;
+ ALOGI("isTV(ro.oem.key1='%s'): %s.", key.c_str(), tv ? "true" : "false");
+ }
+ return tv;
+}
+
+static bool isWear() {
+ static string wearSdkStr = GetProperty("ro.cw_build.wear_sdk.version", "");
+ static int wearSdkInt = GetIntProperty("ro.cw_build.wear_sdk.version", 0);
+ static string buildChars = GetProperty("ro.build.characteristics", "");
+ static vector<string> v = Tokenize(buildChars, ",");
+ static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
+ static bool wear = (wearSdkInt > 0) || watch;
+ static bool logged = false;
+ if (!logged) {
+ logged = true;
+ ALOGI("isWear(ro.cw_build.wear_sdk.version=%d[%s] ro.build.characteristics='%s'): %s",
+ wearSdkInt, wearSdkStr.c_str(), buildChars.c_str(), wear ? "true" : "false");
+ }
+ return wear;
+}
+
+static int doLoad(char** argv, char * const envp[]) {
+ const bool runningAsRoot = !getuid(); // true iff U QPR3 or V+
+
+ // Any released device will have codename REL instead of a 'real' codename.
+ // For safety: default to 'REL' so we default to unreleased=false on failure.
+ const bool unreleased = (GetProperty("ro.build.version.codename", "REL") != "REL");
+
+ // goog/main device_api_level is bumped *way* before aosp/main api level
+ // (the latter only gets bumped during the push of goog/main to aosp/main)
+ //
+ // Since we develop in AOSP, we want it to behave as if it was bumped too.
+ //
+ // Note that AOSP doesn't really have a good api level (for example during
+ // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
+ // One could argue that for our purposes AOSP api level should be infinite or 10000.
+ //
+ // This could also cause api to be increased in goog/main or other branches,
+ // but I can't imagine a case where this would be a problem: the problem
+ // is rather a too low api level, rather than some ill defined high value.
+ // For example as I write this aosp is 34/U, and goog is 35/V,
+ // we want to treat both goog & aosp as 35/V, but it's harmless if we
+ // treat goog as 36 because that value isn't yet defined to mean anything,
+ // and we thus never compare against it.
+ //
+ // Also note that 'android_get_device_api_level()' is what the
+ // //system/core/init/apex_init_util.cpp
+ // apex init .XXrc parsing code uses for XX filtering.
+ //
+ // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
+ // but could (should?) perhaps be adjusted to match this.
+ const int effective_api_level = android_get_device_api_level() + (int)unreleased;
+ const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
+ const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
+ const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
+ const bool isAtLeastW = (effective_api_level > __ANDROID_API_V__); // TODO: switch to W
+
+ const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
+
+ // 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");
+
+ // Version of Network BpfLoader depends on the Android OS version
+ unsigned int bpfloader_ver = BPFLOADER_MAINLINE_VERSION; // [42u]
+ if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
+ if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
+ if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
+ if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
+ if (isAtLeastW) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_W_VERSION
+
+ ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+ bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+ kernelVersion(), describeArch(), getuid(),
+ has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+
+ if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+ ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+ ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ logTetheringApexVersion();
+
+ if (!isAtLeastT) {
+ ALOGE("Impossible - not reachable on Android <T.");
+ return 1;
+ }
+
+ // both S and T require kernel 4.9 (and eBpf support)
+ if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
+ ALOGE("Android T requires kernel 4.9.");
+ return 1;
+ }
+
+ // U bumps the kernel requirement up to 4.14
+ if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
+ ALOGE("Android U requires kernel 4.14.");
+ return 1;
+ }
+
+ // V bumps the kernel requirement up to 4.19
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel419
+ if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
+ ALOGE("Android V requires kernel 4.19.");
+ return 1;
+ }
+
+ // Technically already required by U, but only enforce on V+
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
+ if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
+ ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
+ if (!isTV()) return 1;
+ }
+
+ // 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
+ if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
+ return 1;
+ }
+
+ // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
+ if (isAtLeastV && isKernel32Bit() && isX86()) {
+ ALOGE("Android V requires X86 kernel to be 64-bit.");
+ if (!isTV()) return 1;
+ }
+
+ if (isAtLeastV) {
+ bool bad = false;
+
+ if (!isLtsKernel()) {
+ ALOGW("Android V only supports LTS kernels.");
+ bad = true;
+ }
+
+#define REQUIRE(maj, min, sub) \
+ if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
+ ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
+ bad = true; \
+ }
+
+ REQUIRE(4, 19, 236)
+ REQUIRE(5, 4, 186)
+ REQUIRE(5, 10, 199)
+ REQUIRE(5, 15, 136)
+ REQUIRE(6, 1, 57)
+ REQUIRE(6, 6, 0)
+
+#undef REQUIRE
+
+ if (bad) {
+ ALOGE("Unsupported kernel version (%07x).", kernelVersion());
+ }
+ }
+
+ /* Android 14/U should only launch on 64-bit kernels
+ * T launches on 5.10/5.15
+ * U launches on 5.15/6.1
+ * So >=5.16 implies isKernel64Bit()
+ *
+ * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+ *
+ * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+ * we also require 64-bit userspace.
+ *
+ * There are various known issues with 32-bit userspace talking to various
+ * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+ * Some of these have userspace or kernel workarounds/hacks.
+ * Some of them don't...
+ * We're going to be removing the hacks.
+ * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+ * Note: this check/enforcement only applies to *system* userspace code,
+ * it does not affect unprivileged apps, the 32-on-64 compatibility
+ * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
+ *
+ * Additionally the 32-bit kernel jit support is poor,
+ * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+ */
+ if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
+ // Stuff won't work reliably, but...
+ if (isTV()) {
+ // exempt TVs... they don't really need functional advanced networking
+ ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (isWear() && isArm()) {
+ // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
+ ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+ // also exempt Arm devices upgrading with major kernel rev from T-
+ // might possibly be better for them to run with a newer kernel...
+ ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (isArm()) {
+ ALOGE("[Arm] 64-bit userspace required on 6.2+ kernels (%d).", first_api_level);
+ return 1;
+ } else { // x86 since RiscV cannot be 32-bit
+ ALOGE("[x86] 64-bit userspace required on 6.2+ kernels.");
+ return 1;
+ }
+ }
+
+ // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+ if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ ALOGE("64-bit userspace required on 6.7+ kernels.");
+ return 1;
+ }
+
+ // Ensure we can determine the Android build type.
+ if (!isEng() && !isUser() && !isUserdebug()) {
+ ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
+ getBuildType().c_str());
+ return 1;
+ }
+
+ if (runningAsRoot) {
+ // Note: writing this proc file requires being root (always the case on V+)
+
+ // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
+ // but we need 0 (enabled)
+ // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
+ // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
+ if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
+ isAtLeastKernelVersion(5, 13, 0)) return 1;
+ }
+
+ if (isAtLeastU) {
+ // Note: writing these proc files requires CAP_NET_ADMIN
+ // and sepolicy which is only present on U+,
+ // on Android T and earlier versions they're written from the 'load_bpf_programs'
+ // trigger (ie. by init itself) instead.
+
+ // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
+ // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_BPF_JIT=y)
+ // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+ // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+
+ // Enable JIT kallsyms export for privileged users only
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ }
+
+ // Create all the pin subdirectories
+ // (this must be done first to allow selinux_context and pin_subdir functionality,
+ // which could otherwise fail with ENOENT during object pinning or renaming,
+ // due to ordering issues)
+ for (const auto& location : locations) {
+ 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(bpfloader_ver, location) != 0) {
+ ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
+ ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
+ ALOGE("If this triggers randomly, you might be hitting some memory allocation "
+ "problems or startup script race.");
+ ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
+ sleep(20);
+ return 2;
+ }
+ }
+
+ int key = 1;
+ int value = 123;
+ unique_fd map(
+ createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+ if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
+ ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
+ return 1;
+ }
+
+ // leave a flag that we're done
+ if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
+
+ // platform bpfloader will only succeed when run as root
+ if (!runningAsRoot) {
+ // unreachable on U QPR3+ which always runs netbpfload as root
+
+ ALOGI("mainline done, no need to transfer control to platform bpf loader.");
+ return 0;
+ }
+
+ // unreachable before U QPR3
+ ALOGI("done, transferring control to platform bpfloader.");
+
+ // platform BpfLoader *needs* to run as root
+ const char * args[] = { platformBpfLoader, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
+ return 1;
+}
+
+} // namespace bpf
+} // namespace android
+
+int main(int argc, char** argv, char * const envp[]) {
+ InitLogging(argv, &KernelLogger);
+
+ if (argc == 2 && !strcmp(argv[1], "done")) {
+ // we're being re-exec'ed from platform bpfloader to 'finalize' things
+ if (!SetProperty("bpf.progs_loaded", "1")) {
+ ALOGE("Failed to set bpf.progs_loaded property to 1.");
+ return 125;
+ }
+ ALOGI("success.");
+ return 0;
+ }
+
+ return android::bpf::doLoad(argv, envp);
+}
diff --git a/bpf/loader/initrc-doc/README.txt b/bpf/loader/initrc-doc/README.txt
new file mode 100644
index 0000000..2b22326
--- /dev/null
+++ b/bpf/loader/initrc-doc/README.txt
@@ -0,0 +1,116 @@
+This directory contains comment stripped versions of
+ //system/bpf/bpfloader/bpfloader.rc
+or
+ //packages/modules/Connectivity/bpf/loader/netbpfload.rc
+(as appropriate) from previous versions of Android.
+
+Generated via:
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+ git cat-file -p remotes/aosp/android14-qpr2-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2-24Q1.rc
+ git cat-file -p remotes/aosp/android14-qpr3-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR3-24Q2.rc
+ git cat-file -p remotes/aosp/android15-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk35-15-V-24Q3.rc
+ git cat-file -p remotes/aosp/main:bpf/loader/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk35-15-V-QPR1-24Q4.rc
+
+see also:
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android11-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android12-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android13-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr1-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr2-release/bpfloader/ (rc file is gone in QPR2)
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr2-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr3-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-qpr1-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/main/netbpfload/netbpfload.rc
+or:
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q1-release/netbpfload/netbpfload.rc
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q2-release/netbpfload/netbpfload.rc
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q3-release/netbpfload/netbpfload.rc
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q4-release/bpf/loader/netbpfload.rc
+
+this is entirely equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+it is also equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
+
+Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
+
+
+Key takeaways:
+
+= R bpfloader (platform)
+ - CHOWN + SYS_ADMIN
+ - asynchronous startup
+ - platform only
+ - proc file setup handled by initrc
+
+= S bpfloader (platform)
+ - adds NET_ADMIN
+ - synchronous startup
+ - platform + mainline tethering offload
+
+= T bpfloader (platform)
+ - platform + mainline networking (including tethering offload)
+ - supported btf for maps via exec of btfloader
+
+= U bpfloader (platform)
+ - proc file setup moved into bpfloader binary
+ - explicitly specified user and groups:
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+
+= U QPR2 [24Q1] bpfloader (platform netbpfload -> platform bpfloader)
+ - drops support of btf for maps
+ - invocation of /system/bin/netbpfload binary, which after handling *all*
+ networking bpf related things executes the platform /system/bin/bpfloader
+ which handles non-networking bpf.
+ - Note: this does not (by itself) call into apex NetBpfLoad
+
+= U QPR3 [24Q2] bpfloader (platform netbpfload -> apex netbpfload -> platform bpfloader)
+ - platform NetBpfload *always* execs into apex NetBpfLoad,
+ - shipped with mainline tethering apex that includes NetBpfLoad binary.
+
+= V [24Q3] bpfloader (apex netbpfload -> platform bpfloader)
+ - no significant changes, though it does hard require the apex NetBpfLoad
+ by virtue of the platform NetBpfLoad no longer being present.
+ ie. the apex must override the platform 'bpfloader' service for 35+:
+ the V FRC M-2024-08+ tethering apex does this.
+
+= V QPR1 [24Q4] bpfloader (apex netbpfload -> platform bpfloader)
+ - made netd start earlier (previously happened in parallel to zygote)
+ - renamed and moved the trigger out of netbpload.rc into
+ //system/core/rootdir/init.rc
+ - the new sequence is:
+ trigger post-fs-data (logd available, starts apexd)
+ trigger load-bpf-programs (does: exec_start bpfloader)
+ trigger bpf-progs-loaded (does: start netd)
+ trigger zygote-start
+ - this is more or less irrelevant from the point of view of the bpfloader,
+ but it does mean netd init could fail and abort the boot earlier,
+ before 'A/B update_verifier marks a successful boot'.
+ Though note that due to netd being started asynchronously, it is racy.
+
+Note that there is now a copy of 'netbpfload' provided by the tethering apex
+mainline module at /apex/com.android.tethering/bin/netbpfload, which due
+to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
+added for btf map support (specifically the ability to exec the "btfloader").
+
+= mainline tethering apex M-2024-08+ overrides the platform service for V+
+ thus loading mainline (ie. networking) bpf programs from mainline 'NetBpfLoad'
+ and platform ones from platform 'bpfloader'.
+
+= mainline tethering apex M-2024-09+ changes T+ behaviour (U QPR3+ unaffected)
+ netd -> netd_updatable.so -> ctl.start=mdnsd_netbpfload -> load net bpf programs
diff --git a/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc b/bpf/loader/initrc-doc/bpfloader-sdk30-11-R.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk30-11-R.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc b/bpf/loader/initrc-doc/bpfloader-sdk31-12-S.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk31-12-S.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc b/bpf/loader/initrc-doc/bpfloader-sdk33-13-T.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk33-13-T.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
similarity index 100%
copy from netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
copy to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
new file mode 100644
index 0000000..066cfc8
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
@@ -0,0 +1,8 @@
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/false
+ user root
+ oneshot
+ reboot_on_failure reboot,netbpfload-missing
+ updatable
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
new file mode 100644
index 0000000..e2639ac
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
@@ -0,0 +1,5 @@
+service bpfloader /system/bin/false
+ user root
+ oneshot
+ reboot_on_failure reboot,netbpfload-missing
+ updatable
diff --git a/netbpfload/netbpfload.33rc b/bpf/loader/netbpfload.33rc
similarity index 91%
rename from netbpfload/netbpfload.33rc
rename to bpf/loader/netbpfload.33rc
index d269ce9..eb937dd 100644
--- a/netbpfload/netbpfload.33rc
+++ b/bpf/loader/netbpfload.33rc
@@ -17,5 +17,4 @@
file /dev/kmsg w
rlimit memlock 1073741824 1073741824
oneshot
- # TODO: reboot_on_failure reboot,netbpfload-failed
- override
+ reboot_on_failure reboot,netbpfload-failed
diff --git a/netbpfload/netbpfload.35rc b/bpf/loader/netbpfload.35rc
similarity index 100%
rename from netbpfload/netbpfload.35rc
rename to bpf/loader/netbpfload.35rc
diff --git a/netbpfload/netbpfload.rc b/bpf/loader/netbpfload.rc
similarity index 80%
rename from netbpfload/netbpfload.rc
rename to bpf/loader/netbpfload.rc
index e1af47f..10bfbb2 100644
--- a/netbpfload/netbpfload.rc
+++ b/bpf/loader/netbpfload.rc
@@ -1,22 +1,3 @@
-# zygote-start is what officially starts netd (see //system/core/rootdir/init.rc)
-# However, on some hardware it's started from post-fs-data as well, which is just
-# a tad earlier. There's no benefit to that though, since on 4.9+ P+ devices netd
-# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
-#
-# It is important that we start bpfloader after:
-# - /sys/fs/bpf is already mounted,
-# - apex (incl. rollback) is initialized (so that in the future we can load bpf
-# programs shipped as part of apex mainline modules)
-# - logd is ready for us to log stuff
-#
-# At the same time we want to be as early as possible to reduce races and thus
-# failures (before memory is fragmented, and cpu is busy running tons of other
-# stuff) and we absolutely want to be before netd and the system boot slot is
-# considered to have booted successfully.
-#
-on load_bpf_programs
- exec_start bpfloader
-
# Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
# by virtue of 'service bpfloader' being overridden by the apex shipped .rc
# Warning: most of the below settings are irrelevant unless the apex is missing.
diff --git a/netd/Android.bp b/bpf/netd/Android.bp
similarity index 100%
rename from netd/Android.bp
rename to bpf/netd/Android.bp
diff --git a/netd/BpfBaseTest.cpp b/bpf/netd/BpfBaseTest.cpp
similarity index 97%
rename from netd/BpfBaseTest.cpp
rename to bpf/netd/BpfBaseTest.cpp
index c979a7b..34dfbb4 100644
--- a/netd/BpfBaseTest.cpp
+++ b/bpf/netd/BpfBaseTest.cpp
@@ -56,7 +56,7 @@
TEST_F(BpfBasicTest, TestCgroupMounted) {
std::string cg2_path;
- ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, &cg2_path));
+ ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_HIERARCHY_NAME, &cg2_path));
ASSERT_EQ(0, access(cg2_path.c_str(), R_OK));
ASSERT_EQ(0, access((cg2_path + "/cgroup.controllers").c_str(), R_OK));
}
diff --git a/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
similarity index 93%
rename from netd/BpfHandler.cpp
rename to bpf/netd/BpfHandler.cpp
index 0f84656..8e4c2c6 100644
--- a/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -114,6 +114,11 @@
cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
+ if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
+ cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
+ }
+
if (modules::sdklevel::IsAtLeastV()) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_CONNECT));
@@ -134,19 +139,12 @@
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
cg_fd, BPF_CGROUP_SETSOCKOPT));
}
-
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
- cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
- }
}
if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(
- "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_BIND));
- RETURN_IF_NOT_OK(attachProgramToCgroup(
- "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
cg_fd, BPF_CGROUP_INET6_BIND));
// This should trivially pass, since we just attached up above,
@@ -158,6 +156,10 @@
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
}
+ if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+ }
+
if (modules::sdklevel::IsAtLeastV()) {
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
@@ -170,10 +172,6 @@
if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
}
-
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
- }
}
return netdutils::status::ok;
@@ -203,7 +201,7 @@
}
}
-Status BpfHandler::init(const char* cg2_path) {
+static inline void waitForBpf() {
// Note: netd *can* be restarted, so this might get called a second time after boot is complete
// at which point we don't need to (and shouldn't) wait for (more importantly start) loading bpf
@@ -216,27 +214,36 @@
}
if (!mainlineNetBpfLoadDone()) {
- const bool enforce_mainline = false; // TODO: flip to true
-
// We're on < U QPR3 & it's the first time netd is starting up (unless crashlooping)
//
// On U QPR3+ netbpfload is guaranteed to run before the platform bpfloader,
// so waitForProgsLoaded() implies mainlineNetBpfLoadDone().
if (!base::SetProperty("ctl.start", "mdnsd_netbpfload")) {
ALOGE("Failed to set property ctl.start=mdnsd_netbpfload, see dmesg for reason.");
- if (enforce_mainline) abort();
+ abort();
}
- if (enforce_mainline) {
- ALOGI("Waiting for Networking BPF programs");
- waitForNetProgsLoaded();
- ALOGI("Networking BPF programs are loaded");
- } else {
- ALOGI("Started mdnsd_netbpfload asynchronously.");
- }
+ ALOGI("Waiting for Networking BPF programs");
+ waitForNetProgsLoaded();
+ ALOGI("Networking BPF programs are loaded");
}
ALOGI("BPF programs are loaded");
+}
+
+Status BpfHandler::init(const char* cg2_path) {
+ // This wait is effectively a no-op on U QPR3+ devices (as netd starts
+ // *after* the synchronous 'exec_start bpfloader' which calls NetBpfLoad)
+ // but checking for U QPR3 is hard.
+ //
+ // Waiting should not be required on U QPR3+ devices,
+ // ...
+ //
+ // ...unless someone changed 'exec_start bpfloader' to 'start bpfloader'
+ // in the rc file.
+ //
+ // TODO: should be: if (!modules::sdklevel::IsAtLeastW())
+ if (android_get_device_api_level() <= __ANDROID_API_V__) waitForBpf();
RETURN_IF_NOT_OK(initPrograms(cg2_path));
RETURN_IF_NOT_OK(initMaps());
diff --git a/netd/BpfHandler.h b/bpf/netd/BpfHandler.h
similarity index 100%
rename from netd/BpfHandler.h
rename to bpf/netd/BpfHandler.h
diff --git a/netd/BpfHandlerTest.cpp b/bpf/netd/BpfHandlerTest.cpp
similarity index 100%
rename from netd/BpfHandlerTest.cpp
rename to bpf/netd/BpfHandlerTest.cpp
diff --git a/netd/NetdUpdatable.cpp b/bpf/netd/NetdUpdatable.cpp
similarity index 100%
rename from netd/NetdUpdatable.cpp
rename to bpf/netd/NetdUpdatable.cpp
diff --git a/netd/include/NetdUpdatablePublic.h b/bpf/netd/include/NetdUpdatablePublic.h
similarity index 100%
rename from netd/include/NetdUpdatablePublic.h
rename to bpf/netd/include/NetdUpdatablePublic.h
diff --git a/netd/libnetd_updatable.map.txt b/bpf/netd/libnetd_updatable.map.txt
similarity index 100%
rename from netd/libnetd_updatable.map.txt
rename to bpf/netd/libnetd_updatable.map.txt
diff --git a/bpf_progs/Android.bp b/bpf/progs/Android.bp
similarity index 73%
rename from bpf_progs/Android.bp
rename to bpf/progs/Android.bp
index 9e67415..20d194c 100644
--- a/bpf_progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -38,6 +38,7 @@
cflags: [
"-Wall",
"-Werror",
+ "-Wextra",
],
sdk_version: "30",
min_sdk_version: "30",
@@ -46,8 +47,8 @@
"com.android.tethering",
],
visibility: [
- "//packages/modules/Connectivity/DnsResolver",
- "//packages/modules/Connectivity/netd",
+ "//packages/modules/Connectivity/bpf/dns_helper",
+ "//packages/modules/Connectivity/bpf/netd",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service/native/libs/libclat",
"//packages/modules/Connectivity/Tethering",
@@ -63,85 +64,42 @@
// bpf kernel programs
//
bpf {
- name: "block.o",
- srcs: ["block.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
- sub_dir: "net_shared",
-}
-
-bpf {
name: "dscpPolicy.o",
srcs: ["dscpPolicy.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
sub_dir: "net_shared",
}
-bpf {
- name: "gentle.o",
- srcs: ["gentle.c"],
- cflags: [
- "-Wall",
- "-Werror",
- ],
- sub_dir: "net_shared",
-}
-
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "offload.o",
srcs: ["offload.c"],
- cflags: [
- "-Wall",
- "-Werror",
- ],
+ btf: false,
}
+// This version ships to Android T+ which uses mainline netbpfload.
bpf {
name: "offload@mainline.o",
srcs: ["offload@mainline.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- "-DMAINLINE",
- ],
+ cflags: ["-DMAINLINE"],
}
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "test.o",
srcs: ["test.c"],
- cflags: [
- "-Wall",
- "-Werror",
- ],
+ btf: false,
}
+// This version ships to Android T+ which uses mainline netbpfload.
bpf {
name: "test@mainline.o",
srcs: ["test@mainline.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- "-DMAINLINE",
- ],
+ cflags: ["-DMAINLINE"],
}
bpf {
name: "clatd.o",
srcs: ["clatd.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
sub_dir: "net_shared",
}
@@ -149,11 +107,6 @@
// WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
name: "netd.o",
srcs: ["netd.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
// WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
sub_dir: "netd_shared",
}
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
similarity index 60%
rename from bpf_progs/bpf_net_helpers.h
rename to bpf/progs/bpf_net_helpers.h
index 1511ee5..a5664ba 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -17,21 +17,72 @@
#pragma once
#include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
#include <linux/if_packet.h>
-#include <stdbool.h>
-#include <stdint.h>
-
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
// bionic kernel uapi linux/udp.h header is munged...
#define __kernel_udphdr udphdr
#include <linux/udp.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+// IP flags. (from kernel's include/net/ip.h)
+#define IP_CE 0x8000 // Flag: "Congestion" (really reserved 'evil bit')
+#define IP_DF 0x4000 // Flag: "Don't Fragment"
+#define IP_MF 0x2000 // Flag: "More Fragments"
+#define IP_OFFSET 0x1FFF // "Fragment Offset" part
+
+// IPv6 fragmentation header. (from kernel's include/net/ipv6.h)
+struct frag_hdr {
+ __u8 nexthdr;
+ __u8 reserved; // always zero
+ __be16 frag_off; // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
+ __be32 identification;
+};
+
+// ----- Helper functions for offsets to fields -----
+
+// They all assume simple IP packets:
+// - no VLAN ethernet tags
+// - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
+// - no IPv6 extension headers
+// - no TCP options (see TCP_HLEN)
+
+//#define ETH_HLEN sizeof(struct ethhdr)
+#define IP4_HLEN sizeof(struct iphdr)
+#define IP6_HLEN sizeof(struct ipv6hdr)
+#define TCP_HLEN sizeof(struct tcphdr)
+#define UDP_HLEN sizeof(struct udphdr)
// Offsets from beginning of L4 (TCP/UDP) header
#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
#define UDP_OFFSET(field) offsetof(struct udphdr, field)
-// Offsets from beginning of L3 (IPv4/IPv6) header
+// Offsets from beginning of L3 (IPv4) header
#define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
+#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L3 (IPv6) header
#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
+#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
+#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
+#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
+#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
+#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
+#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
+#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
// this returns 0 iff skb->sk is NULL
static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
@@ -83,11 +134,29 @@
// try to make the first 'len' header bytes readable/writable via direct packet access
// (note: AFAIK there is no way to ask for only direct packet read without also getting write)
-static inline __always_inline void try_make_writable(struct __sk_buff* skb, int len) {
+static inline __always_inline void try_make_writable(struct __sk_buff* skb, unsigned len) {
if (len > skb->len) len = skb->len;
if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
}
+// anti-compiler-optimizer no-op: explicitly force full calculation of 'v'
+//
+// The use for this is to force full calculation of a complex arithmetic (likely binary
+// bitops) value, and then check the result only once (thus likely reducing the number
+// of required conditional jump instructions that badly affect bpf verifier runtime)
+//
+// The compiler cannot look into the assembly statement, so it doesn't know it does nothing.
+// Since the statement takes 'v' as both input and output in a register (+r),
+// the compiler must fully calculate the precise value of 'v' before this,
+// and must use the (possibly modified) value of 'v' afterwards (thus cannot
+// do funky optimizations to use partial results from before the asm).
+//
+// As this is not flagged 'volatile' this may still be moved out of a loop,
+// or even entirely optimized out if 'v' is never used afterwards.
+//
+// See: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
+#define COMPILER_FORCE_CALCULATION(v) asm ("" : "+r" (v))
+
struct egress_bool { bool egress; };
#define INGRESS ((struct egress_bool){ .egress = false })
#define EGRESS ((struct egress_bool){ .egress = true })
@@ -103,3 +172,10 @@
struct updatetime_bool { bool updatetime; };
#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
+
+// Return value for xt_bpf (netfilter match extension) programs
+static const int XTBPF_NOMATCH = 0;
+static const int XTBPF_MATCH = 1;
+
+static const int BPF_DISALLOW = 0;
+static const int BPF_ALLOW = 1;
diff --git a/bpf_progs/clat_mark.h b/bpf/progs/clat_mark.h
similarity index 100%
rename from bpf_progs/clat_mark.h
rename to bpf/progs/clat_mark.h
diff --git a/bpf_progs/clatd.c b/bpf/progs/clatd.c
similarity index 93%
rename from bpf_progs/clatd.c
rename to bpf/progs/clatd.c
index 95e9a4c..2d4551e 100644
--- a/bpf_progs/clatd.c
+++ b/bpf/progs/clatd.c
@@ -14,44 +14,13 @@
* limitations under the License.
*/
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/swab.h>
-#include <stdbool.h>
-#include <stdint.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "clatd.h"
#include "clat_mark.h"
-// IP flags. (from kernel's include/net/ip.h)
-#define IP_CE 0x8000 // Flag: "Congestion" (really reserved 'evil bit')
-#define IP_DF 0x4000 // Flag: "Don't Fragment"
-#define IP_MF 0x2000 // Flag: "More Fragments"
-#define IP_OFFSET 0x1FFF // "Fragment Offset" part
-
-// from kernel's include/net/ipv6.h
-struct frag_hdr {
- __u8 nexthdr;
- __u8 reserved; // always zero
- __be16 frag_off; // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
- __be32 identification;
-};
-
DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
static inline __always_inline int nat64(struct __sk_buff* skb,
@@ -177,7 +146,7 @@
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
- for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
sum4 += ((__u16*)&ip)[i];
}
// Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4
@@ -188,7 +157,7 @@
// Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header.
__wsum sum6 = 0;
// We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits)
- for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
sum6 += ~((__u16*)ip6)[i]; // note the bitwise negation
}
@@ -321,7 +290,7 @@
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
- for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
sum4 += ((__u16*)ip4)[i];
}
// Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -387,7 +356,7 @@
// Calculate the IPv6 16-bit one's complement checksum of the IPv6 header.
__wsum sum6 = 0;
// We'll end up with a non-zero sum due to ip6.version == 6
- for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
sum6 += ((__u16*)&ip6)[i];
}
@@ -430,5 +399,3 @@
LICENSE("Apache 2.0");
CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
-DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/clatd.h b/bpf/progs/clatd.h
similarity index 100%
rename from bpf_progs/clatd.h
rename to bpf/progs/clatd.h
diff --git a/bpf/progs/dscpPolicy.c b/bpf/progs/dscpPolicy.c
new file mode 100644
index 0000000..94d717b
--- /dev/null
+++ b/bpf/progs/dscpPolicy.c
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+
+#include "bpf_net_helpers.h"
+#include "dscpPolicy.h"
+
+#define ECN_MASK 3
+#define UPDATE_TOS(dscp, tos) ((dscp) << 2) | ((tos) & ECN_MASK)
+
+// The cache is never read nor written by userspace and is indexed by socket cookie % CACHE_MAP_SIZE
+#define CACHE_MAP_SIZE 32 // should be a power of two so we can % cheaply
+DEFINE_BPF_MAP_KERNEL_INTERNAL(socket_policy_cache_map, PERCPU_ARRAY, uint32_t, RuleEntry,
+ CACHE_MAP_SIZE)
+
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+
+static inline __always_inline uint64_t calculate_u64(uint64_t v) {
+ COMPILER_FORCE_CALCULATION(v);
+ return v;
+}
+
+static inline __always_inline void match_policy(struct __sk_buff* skb, const bool ipv4) {
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+
+ const int l2_header_size = sizeof(struct ethhdr);
+ struct ethhdr* eth = data;
+
+ if (data + l2_header_size > data_end) return;
+
+ int hdr_size = 0;
+
+ // used for map lookup
+ uint64_t cookie = bpf_get_socket_cookie(skb);
+ if (!cookie) return;
+
+ uint32_t cacheid = cookie % CACHE_MAP_SIZE;
+
+ __be16 sport = 0;
+ uint16_t dport = 0;
+ uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
+ struct in6_addr src_ip = {};
+ struct in6_addr dst_ip = {};
+ uint8_t tos = 0; // Only used for IPv4
+ __be32 old_first_be32 = 0; // Only used for IPv6
+ if (ipv4) {
+ const struct iphdr* const iph = (void*)(eth + 1);
+ hdr_size = l2_header_size + sizeof(struct iphdr);
+ // Must have ipv4 header
+ if (data + hdr_size > data_end) return;
+
+ // IP version must be 4
+ if (iph->version != 4) return;
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (iph->ihl != 5) return;
+
+ // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+ src_ip.s6_addr32[2] = htonl(0x0000ffff);
+ dst_ip.s6_addr32[2] = htonl(0x0000ffff);
+
+ // Copy IPv4 address into in6_addr for easy comparison below.
+ src_ip.s6_addr32[3] = iph->saddr;
+ dst_ip.s6_addr32[3] = iph->daddr;
+ protocol = iph->protocol;
+ tos = iph->tos;
+ } else {
+ struct ipv6hdr* ip6h = (void*)(eth + 1);
+ hdr_size = l2_header_size + sizeof(struct ipv6hdr);
+ // Must have ipv6 header
+ if (data + hdr_size > data_end) return;
+
+ if (ip6h->version != 6) return;
+
+ src_ip = ip6h->saddr;
+ dst_ip = ip6h->daddr;
+ protocol = ip6h->nexthdr;
+ old_first_be32 = *(__be32*)ip6h;
+ }
+
+ switch (protocol) {
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE: {
+ struct udphdr* udp;
+ udp = data + hdr_size;
+ if ((void*)(udp + 1) > data_end) return;
+ sport = udp->source;
+ dport = ntohs(udp->dest);
+ } break;
+ case IPPROTO_TCP: {
+ struct tcphdr* tcp;
+ tcp = data + hdr_size;
+ if ((void*)(tcp + 1) > data_end) return;
+ sport = tcp->source;
+ dport = ntohs(tcp->dest);
+ } break;
+ default:
+ return;
+ }
+
+ // this array lookup cannot actually fail
+ RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cacheid);
+
+ if (!existing_rule) return; // impossible
+
+ uint64_t nomatch = 0;
+ nomatch |= v6_not_equal(src_ip, existing_rule->src_ip);
+ nomatch |= v6_not_equal(dst_ip, existing_rule->dst_ip);
+ nomatch |= (skb->ifindex ^ existing_rule->ifindex);
+ nomatch |= (sport ^ existing_rule->src_port);
+ nomatch |= (dport ^ existing_rule->dst_port);
+ nomatch |= (protocol ^ existing_rule->proto);
+ COMPILER_FORCE_CALCULATION(nomatch);
+
+ /*
+ * After the above funky bitwise arithmetic we have 'nomatch == 0' iff
+ * src_ip == existing_rule->src_ip &&
+ * dst_ip == existing_rule->dst_ip &&
+ * skb->ifindex == existing_rule->ifindex &&
+ * sport == existing_rule->src_port &&
+ * dport == existing_rule->dst_port &&
+ * protocol == existing_rule->proto
+ */
+
+ if (!nomatch) {
+ if (existing_rule->dscp_val < 0) return; // cached no-op
+
+ if (ipv4) {
+ uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
+ bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(newTos),
+ sizeof(uint16_t));
+ bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &newTos, sizeof(newTos), 0);
+ } else {
+ __be32 new_first_be32 =
+ htonl(ntohl(old_first_be32) & 0xF03FFFFF | (existing_rule->dscp_val << 22));
+ bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
+ BPF_F_RECOMPUTE_CSUM);
+ }
+ return; // cached DSCP mutation
+ }
+
+ // Linear scan ipv?_dscp_policies_map since stored params didn't match skb.
+ uint64_t best_score = 0;
+ int8_t new_dscp = -1; // meaning no mutation
+
+ for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+ // Using a uint64 in for loop prevents infinite loop during BPF load,
+ // but the key is uint32, so convert back.
+ uint32_t key = i;
+
+ DscpPolicy* policy;
+ if (ipv4) {
+ policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+ } else {
+ policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
+ }
+
+ // Lookup failure cannot happen on an array with MAX_POLICIES entries.
+ // While 'continue' would make logical sense here, 'return' should be
+ // easier for the verifier to analyze.
+ if (!policy) return;
+
+ // Think of 'nomatch' as a 64-bit boolean: false iff zero, true iff non-zero.
+ // Start off with nomatch being false, ie. we assume things *are* matching.
+ uint64_t nomatch = 0;
+
+ // Due to 'a ^ b' being 0 iff a == b:
+ // nomatch |= a ^ b
+ // should/can be read as:
+ // nomatch ||= (a != b)
+ // which you can also think of as:
+ // match &&= (a == b)
+
+ // If policy iface index does not match skb, then skip to next policy.
+ nomatch |= (policy->ifindex ^ skb->ifindex);
+
+ // policy->match_* are normal booleans, and should thus always be 0 or 1,
+ // thus you can think of these as:
+ // if (policy->match_foo) match &&= (foo == policy->foo);
+ nomatch |= policy->match_proto * (protocol ^ policy->proto);
+ nomatch |= policy->match_src_ip * v6_not_equal(src_ip, policy->src_ip);
+ nomatch |= policy->match_dst_ip * v6_not_equal(dst_ip, policy->dst_ip);
+ nomatch |= policy->match_src_port * (sport ^ policy->src_port);
+
+ // Since these values are u16s (<=63 bits), we can rely on u64 subtraction
+ // underflow setting the topmost bit. Basically, you can think of:
+ // nomatch |= (a - b) >> 63
+ // as:
+ // match &&= (a >= b)
+ uint64_t dport64 = dport; // Note: dst_port_{start_end} range is inclusive of both ends.
+ nomatch |= calculate_u64(dport64 - policy->dst_port_start) >> 63;
+ nomatch |= calculate_u64(policy->dst_port_end - dport64) >> 63;
+
+ // score is 0x10000 for each matched field (proto, src_ip, dst_ip, src_port)
+ // plus 1..0x10000 for the dst_port range match (smaller for bigger ranges)
+ uint64_t score = 0;
+ score += policy->match_proto; // reminder: match_* are boolean, thus 0 or 1
+ score += policy->match_src_ip;
+ score += policy->match_dst_ip;
+ score += policy->match_src_port;
+ score += 1; // for a 1 element dst_port_{start,end} range
+ score <<= 16; // scale up: ie. *= 0x10000
+ // now reduce score if the dst_port range is more than a single element
+ // we want to prioritize (ie. better score) matches of smaller ranges
+ score -= (policy->dst_port_end - policy->dst_port_start); // -= 0..0xFFFF
+
+ // Here we need:
+ // match &&= (score > best_score)
+ // which is the same as
+ // match &&= (score >= best_score + 1)
+ // > not >= because we want equal score matches to prefer choosing earlier policies
+ nomatch |= calculate_u64(score - best_score - 1) >> 63;
+
+ COMPILER_FORCE_CALCULATION(nomatch);
+ if (nomatch) continue;
+
+ // only reachable if we matched the policy and (score > best_score)
+ best_score = score;
+ new_dscp = policy->dscp_val;
+ }
+
+ // Update cache with found policy.
+ *existing_rule = (RuleEntry){
+ .src_ip = src_ip,
+ .dst_ip = dst_ip,
+ .ifindex = skb->ifindex,
+ .src_port = sport,
+ .dst_port = dport,
+ .proto = protocol,
+ .dscp_val = new_dscp,
+ };
+
+ if (new_dscp < 0) return;
+
+ // Need to store bytes after updating map or program will not load.
+ if (ipv4) {
+ uint8_t new_tos = UPDATE_TOS(new_dscp, tos);
+ bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(new_tos), 2);
+ bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &new_tos, sizeof(new_tos), 0);
+ } else {
+ __be32 new_first_be32 = htonl(ntohl(old_first_be32) & 0xF03FFFFF | (new_dscp << 22));
+ bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
+ BPF_F_RECOMPUTE_CSUM);
+ }
+ return;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
+ KVER_5_15)
+(struct __sk_buff* skb) {
+ if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
+
+ if (skb->protocol == htons(ETH_P_IP)) {
+ match_policy(skb, true);
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ match_policy(skb, false);
+ }
+
+ // Always return TC_ACT_PIPE
+ return TC_ACT_PIPE;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("Connectivity");
diff --git a/bpf_progs/dscpPolicy.h b/bpf/progs/dscpPolicy.h
similarity index 60%
rename from bpf_progs/dscpPolicy.h
rename to bpf/progs/dscpPolicy.h
index e565966..413fb0f 100644
--- a/bpf_progs/dscpPolicy.h
+++ b/bpf/progs/dscpPolicy.h
@@ -14,14 +14,8 @@
* limitations under the License.
*/
-#define CACHE_MAP_SIZE 1024
#define MAX_POLICIES 16
-#define SRC_IP_MASK_FLAG 1
-#define DST_IP_MASK_FLAG 2
-#define SRC_PORT_MASK_FLAG 4
-#define PROTO_MASK_FLAG 8
-
#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
// Retrieve the first (ie. high) 64 bits of an IPv6 address (in network order)
@@ -34,20 +28,6 @@
#define v6_not_equal(a, b) ((v6_hi_be64(a) ^ v6_hi_be64(b)) \
| (v6_lo_be64(a) ^ v6_lo_be64(b)))
-// Returns 'a == b' as boolean
-#define v6_equal(a, b) (!v6_not_equal((a), (b)))
-
-// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
-// smove to common location in future.
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
- (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
- __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
- __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
- (void*)BPF_FUNC_skb_ecn_set_ce;
-
typedef struct {
struct in6_addr src_ip;
struct in6_addr dst_ip;
@@ -57,10 +37,12 @@
uint16_t dst_port_end;
uint8_t proto;
int8_t dscp_val; // -1 none, or 0..63 DSCP value
- uint8_t present_fields;
- uint8_t pad[3];
+ bool match_src_ip;
+ bool match_dst_ip;
+ bool match_src_port;
+ bool match_proto;
} DscpPolicy;
-STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3); // 48
+STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 6 * 1); // 48
typedef struct {
struct in6_addr src_ip;
@@ -72,4 +54,4 @@
int8_t dscp_val; // -1 none, or 0..63 DSCP value
uint8_t pad[2];
} RuleEntry;
-STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2); // 44
+STRUCT_SIZE(RuleEntry, 2 * 16 + 4 + 2 * 2 + 4 * 1); // 44
diff --git a/bpf_progs/netd.c b/bpf/progs/netd.c
similarity index 91%
rename from bpf_progs/netd.c
rename to bpf/progs/netd.c
index 5ce2227..cbe856d 100644
--- a/bpf_progs/netd.c
+++ b/bpf/progs/netd.c
@@ -15,21 +15,8 @@
*/
// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-#include <bpf_helpers.h>
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <stdbool.h>
-#include <stdint.h>
#include "bpf_net_helpers.h"
#include "netd.h"
@@ -38,10 +25,6 @@
static const int PASS = 1;
static const int DROP_UNLESS_DNS = 2; // internal to our program
-// This is used for xt_bpf program only.
-static const int BPF_NOMATCH = 0;
-static const int BPF_MATCH = 1;
-
// Used for 'bool enable_tracing'
static const bool TRACE_ON = true;
static const bool TRACE_OFF = false;
@@ -86,6 +69,8 @@
// TODO: consider whether we can merge some of these maps
// for example it might be possible to merge 2 or 3 of:
// uid_counterset_map + uid_owner_map + uid_permission_map
+DEFINE_BPF_MAP_NO_NETD(blocked_ports_map, ARRAY, int, uint64_t,
+ 1024 /* 64K ports -> 1024 u64s */)
DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
@@ -106,13 +91,13 @@
// A single-element configuration array, packet tracing is enabled when 'true'.
DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
- BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG)
// A ring buffer on which packet information is pushed.
DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
- BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG);
DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
@@ -184,7 +169,7 @@
static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
const TypeOfKey* const key, \
const struct egress_bool egress, \
- const struct kver_uint kver) { \
+ __unused const struct kver_uint kver) { \
StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \
if (!value) { \
StatsValue newValue = {}; \
@@ -524,22 +509,12 @@
return match;
}
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
- BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
- "fs_bpf_netd_readonly", "",
- IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
@@ -556,22 +531,12 @@
return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
}
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
- BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
- "fs_bpf_netd_readonly", "",
- IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
@@ -599,12 +564,12 @@
if (sock_uid == AID_SYSTEM) {
uint64_t cookie = bpf_get_socket_cookie(skb);
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
- if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH;
+ if (utag && utag->uid == AID_CLAT) return XTBPF_NOMATCH;
}
uint32_t key = skb->ifindex;
update_iface_stats_map(skb, &key, EGRESS, KVER_NONE);
- return BPF_MATCH;
+ return XTBPF_MATCH;
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -617,7 +582,7 @@
uint32_t key = skb->ifindex;
update_iface_stats_map(skb, &key, INGRESS, KVER_NONE);
- return BPF_MATCH;
+ return XTBPF_MATCH;
}
DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
@@ -635,7 +600,7 @@
DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
- if (is_system_uid(sock_uid)) return BPF_MATCH;
+ if (is_system_uid(sock_uid)) return XTBPF_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
@@ -643,11 +608,11 @@
// 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;
+ if (sock_uid == 65534) return XTBPF_MATCH;
UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
- if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
- return BPF_NOMATCH;
+ if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? XTBPF_MATCH : XTBPF_NOMATCH;
+ return XTBPF_NOMATCH;
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -656,8 +621,8 @@
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
uint32_t penalty_box = PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH;
- if (denylistMatch) return denylistMatch->rule & penalty_box ? BPF_MATCH : BPF_NOMATCH;
- return BPF_NOMATCH;
+ if (denylistMatch) return denylistMatch->rule & penalty_box ? XTBPF_MATCH : XTBPF_NOMATCH;
+ return XTBPF_NOMATCH;
}
static __always_inline inline uint8_t get_app_permissions() {
@@ -676,13 +641,12 @@
DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
KVER_4_14)
-(struct bpf_sock* sk) {
- // A return value of 1 means allow, everything else means deny.
- return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
+(__unused struct bpf_sock* sk) {
+ return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
}
-DEFINE_NETD_V_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
- inet_socket_release, KVER_5_10)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
+ inet_socket_release, KVER_5_10)
(struct bpf_sock* sk) {
uint64_t cookie = bpf_get_sk_cookie(sk);
if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
@@ -690,7 +654,7 @@
return 1;
}
-static __always_inline inline int check_localhost(struct bpf_sock_addr *ctx) {
+static __always_inline inline int check_localhost(__unused struct bpf_sock_addr *ctx) {
// See include/uapi/linux/bpf.h:
//
// struct bpf_sock_addr {
@@ -705,7 +669,44 @@
// __u32 msg_src_ip6[4]; // BE, R: 1,2,4,8-byte, W: 4,8-byte
// __bpf_md_ptr(struct bpf_sock *, sk);
// };
- return 1;
+ return BPF_ALLOW;
+}
+
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+ if (!ctx->user_port) return BPF_ALLOW;
+
+ switch (ctx->protocol) {
+ case IPPROTO_TCP:
+ case IPPROTO_MPTCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_DCCP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return BPF_ALLOW; // unknown protocols are allowed
+ }
+
+ int key = ctx->user_port >> 6;
+ int shift = ctx->user_port & 63;
+
+ uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+ // Lookup should never fail in reality, but if it does return here to keep the
+ // BPF verifier happy.
+ if (!val) return BPF_ALLOW;
+
+ if ((*val >> shift) & 1) return BPF_DISALLOW;
+ return BPF_ALLOW;
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
}
DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
@@ -743,7 +744,7 @@
// Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
// This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
ctx->optlen = 0;
- return 1; // ALLOW
+ return BPF_ALLOW;
}
DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
@@ -751,10 +752,8 @@
// Tell kernel to use/process original buffer provided by userspace.
// This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
ctx->optlen = 0;
- return 1; // ALLOW
+ return BPF_ALLOW;
}
LICENSE("Apache 2.0");
CRITICAL("Connectivity and netd");
-DISABLE_BTF_ON_USER_BUILDS();
-DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/netd.h b/bpf/progs/netd.h
similarity index 98%
rename from bpf_progs/netd.h
rename to bpf/progs/netd.h
index 4877a4b..be7c311 100644
--- a/bpf_progs/netd.h
+++ b/bpf/progs/netd.h
@@ -157,6 +157,8 @@
#define CGROUP_INET_CREATE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
#define CGROUP_INET_RELEASE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsockrelease_inet_release"
+#define CGROUP_BIND4_PROG_PATH BPF_NETD_PATH "prog_netd_bind4_inet4_bind"
+#define CGROUP_BIND6_PROG_PATH BPF_NETD_PATH "prog_netd_bind6_inet6_bind"
#define CGROUP_CONNECT4_PROG_PATH BPF_NETD_PATH "prog_netd_connect4_inet4_connect"
#define CGROUP_CONNECT6_PROG_PATH BPF_NETD_PATH "prog_netd_connect6_inet6_connect"
#define CGROUP_UDP4_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg4_udp4_recvmsg"
diff --git a/bpf_progs/offload.c b/bpf/progs/offload.c
similarity index 88%
rename from bpf_progs/offload.c
rename to bpf/progs/offload.c
index c7ac059..631908a 100644
--- a/bpf_progs/offload.c
+++ b/bpf/progs/offload.c
@@ -14,82 +14,27 @@
* limitations under the License.
*/
-#include <linux/if.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
#ifdef MAINLINE
// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
// ship a different file than for later versions, but we need bpfloader v0.25+
// for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
#else /* MAINLINE */
-// The resulting .o needs to load on the Android S & T bpfloaders
+// The resulting .o needs to load on the Android S bpfloader
#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
+#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
#endif /* MAINLINE */
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
-#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "offload.h"
-// From kernel:include/net/ip.h
-#define IP_DF 0x4000 // Flag: "Don't Fragment"
-
-// ----- Helper functions for offsets to fields -----
-
-// They all assume simple IP packets:
-// - no VLAN ethernet tags
-// - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
-// - no IPv6 extension headers
-// - no TCP options (see TCP_HLEN)
-
-//#define ETH_HLEN sizeof(struct ethhdr)
-#define IP4_HLEN sizeof(struct iphdr)
-#define IP6_HLEN sizeof(struct ipv6hdr)
-#define TCP_HLEN sizeof(struct tcphdr)
-#define UDP_HLEN sizeof(struct udphdr)
-
-// Offsets from beginning of L4 (TCP/UDP) header
-#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
-#define UDP_OFFSET(field) offsetof(struct udphdr, field)
-
-// Offsets from beginning of L3 (IPv4) header
-#define IP4_OFFSET(field) offsetof(struct iphdr, field)
-#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
-#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L3 (IPv6) header
-#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
-#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
-#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
-#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
-#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
-#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
-#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
-#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
-#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
-
// ----- Tethering Error Counters -----
// Note that pre-T devices with Mediatek chipsets may have a kernel bug (bad patch
// "[ALPS05162612] bpf: fix ubsan error") making it impossible to write to non-zero
// offset of bpf map ARRAYs. This file (offload.o) loads on S+, but luckily this
// array is only written by bpf code, and only read by userspace.
-DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, TETHERING_GID)
+DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, AID_NETWORK_STACK)
#define COUNT_AND_RETURN(counter, ret) do { \
uint32_t code = BPF_TETHER_ERR_ ## counter; \
@@ -107,27 +52,27 @@
// ----- Tethering Data Stats and Limits -----
// Tethering stats, indexed by upstream interface.
-DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK)
// Tethering data limit, indexed by upstream interface.
// (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif])
-DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK)
// ----- IPv6 Support -----
DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
- TETHERING_GID)
+ AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
- 1024, TETHERING_GID)
+ 1024, AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
- TETHERING_GID)
+ AID_NETWORK_STACK)
static inline __always_inline int do_forward6(struct __sk_buff* skb,
const struct rawip_bool rawip,
const struct stream_bool stream,
- const struct kver_uint kver) {
+ __unused const struct kver_uint kver) {
const bool is_ethernet = !rawip.rawip;
// Must be meta-ethernet IPv6 frame
@@ -140,9 +85,8 @@
// Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
// not trigger and thus we need to manually make sure we can read packet headers via DPA.
- // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
// It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
- try_make_writable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+ if (bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN)) return TC_ACT_PIPE;
void* data = (void*)(long)skb->data;
const void* data_end = (void*)(long)skb->data_end;
@@ -165,6 +109,14 @@
// If hardware offload is running and programming flows based on conntrack entries,
// try not to interfere with it.
if (ip6->nexthdr == IPPROTO_TCP) {
+ // don't need to check return code, as it's effectively checked in the next 'if' below
+ bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+ eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
struct tcphdr* tcph = (void*)(ip6 + 1);
// Make sure we can get at the tcp header
@@ -302,13 +254,13 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
-DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream6_ether)
(struct __sk_buff* skb) {
return do_forward6(skb, ETHER, DOWNSTREAM, KVER_NONE);
}
-DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream6_ether)
(struct __sk_buff* skb) {
return do_forward6(skb, ETHER, UPSTREAM, KVER_NONE);
@@ -328,42 +280,42 @@
// and in system/netd/tests/binder_test.cpp NetdBinderTest TetherOffloadForwarding.
//
// Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
}
// and define no-op stubs for pre-4.14 kernels.
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
// ----- IPv4 Support -----
-DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
-DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
const int l2_header_size, void* data, const void* data_end,
struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
const struct stream_bool stream, const struct updatetime_bool updatetime,
- const bool is_tcp, const struct kver_uint kver) {
+ const bool is_tcp, __unused const struct kver_uint kver) {
const bool is_ethernet = !rawip.rawip;
struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -593,7 +545,7 @@
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
- for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
sum4 += ((__u16*)ip)[i];
}
// Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -656,25 +608,25 @@
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
@@ -684,7 +636,7 @@
// (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -692,7 +644,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -700,7 +652,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -708,7 +660,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -729,13 +681,13 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
@@ -745,7 +697,7 @@
// [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_4_14,
KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
@@ -753,7 +705,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_4_14,
KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
@@ -762,13 +714,13 @@
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
@@ -778,43 +730,43 @@
// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
// ETHER: 4.9-P/Q kernel
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
// ----- XDP Support -----
-DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK)
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
- const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward6(__unused struct xdp_md *ctx,
+ __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
- const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward4(__unused struct xdp_md *ctx,
+ __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
return XDP_PASS;
}
@@ -853,7 +805,7 @@
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
@@ -877,5 +829,3 @@
LICENSE("Apache 2.0");
CRITICAL("Connectivity (Tethering)");
-DISABLE_BTF_ON_USER_BUILDS();
-DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/offload.h b/bpf/progs/offload.h
similarity index 100%
rename from bpf_progs/offload.h
rename to bpf/progs/offload.h
diff --git a/bpf_progs/offload@mainline.c b/bpf/progs/offload@mainline.c
similarity index 100%
rename from bpf_progs/offload@mainline.c
rename to bpf/progs/offload@mainline.c
diff --git a/bpf_progs/test.c b/bpf/progs/test.c
similarity index 63%
rename from bpf_progs/test.c
rename to bpf/progs/test.c
index 6a4471c..8585118 100644
--- a/bpf_progs/test.c
+++ b/bpf/progs/test.c
@@ -14,10 +14,6 @@
* limitations under the License.
*/
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/ip.h>
-
#ifdef MAINLINE
// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
// ship a different file than for later versions, but we need bpfloader v0.25+
@@ -29,46 +25,30 @@
#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
#endif /* MAINLINE */
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
// This is non production code, only used for testing
// Needed because the bitmap array definition is non-kosher for pre-T OS devices.
#define THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
-#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "offload.h"
// Used only by TetheringPrivilegedTests, not by production code.
DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
- TETHERING_GID)
+ AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether2_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
- TETHERING_GID)
+ AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether3_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
- TETHERING_GID)
+ AID_NETWORK_STACK)
// Used only by BpfBitmapTest, not by production code.
-DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, AID_NETWORK_STACK)
-DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
- xdp_test, KVER_5_9)
-(struct xdp_md *ctx) {
- void *data = (void *)(long)ctx->data;
- void *data_end = (void *)(long)ctx->data_end;
-
- struct ethhdr *eth = data;
- int hsize = sizeof(*eth);
-
- struct iphdr *ip = data + hsize;
- hsize += sizeof(struct iphdr);
-
- if (data + hsize > data_end) return XDP_PASS;
- if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;
- if (ip->protocol == IPPROTO_UDP) return XDP_DROP;
- return XDP_PASS;
+// we need at least 1 bpf program in the final .o for Android S bpfloader compatibility
+// this program is trivial, and has a 'infinite' minimum kernel version number,
+// so will always be skipped
+DEFINE_BPF_PROG_KVER("skfilter/match", AID_ROOT, AID_ROOT, match, KVER_INF)
+(__unused struct __sk_buff* skb) {
+ return XTBPF_MATCH;
}
LICENSE("Apache 2.0");
-DISABLE_BTF_ON_USER_BUILDS();
+CRITICAL("Networking xTS tests");
diff --git a/bpf_progs/test@mainline.c b/bpf/progs/test@mainline.c
similarity index 100%
rename from bpf_progs/test@mainline.c
rename to bpf/progs/test@mainline.c
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/bpf/syscall_wrappers/Android.bp
similarity index 100%
rename from staticlibs/native/bpf_syscall_wrappers/Android.bp
rename to bpf/syscall_wrappers/Android.bp
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
similarity index 100%
rename from staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
rename to bpf/syscall_wrappers/include/BpfSyscallWrappers.h
diff --git a/tests/mts/Android.bp b/bpf/tests/mts/Android.bp
similarity index 97%
rename from tests/mts/Android.bp
rename to bpf/tests/mts/Android.bp
index c118d0a..9d158fd 100644
--- a/tests/mts/Android.bp
+++ b/bpf/tests/mts/Android.bp
@@ -40,6 +40,6 @@
srcs: [
"bpf_existence_test.cpp",
],
- compile_multilib: "first",
+ compile_multilib: "both",
min_sdk_version: "30", // Ensure test runs on R and above.
}
diff --git a/tests/mts/OWNERS b/bpf/tests/mts/OWNERS
similarity index 100%
rename from tests/mts/OWNERS
rename to bpf/tests/mts/OWNERS
diff --git a/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
similarity index 92%
rename from tests/mts/bpf_existence_test.cpp
rename to bpf/tests/mts/bpf_existence_test.cpp
index b02c61c..0b5b7be 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -80,22 +80,15 @@
TETHERING "prog_offload_schedcls_tether_upstream6_rawip",
};
-// Provided by *current* mainline module for S+ devices with 5.10+ kernels
-static const set<string> MAINLINE_FOR_S_5_10_PLUS = {
- TETHERING "prog_test_xdp_drop_ipv4_udp_ether",
-};
-
// Provided by *current* mainline module for T+ devices
static const set<string> MAINLINE_FOR_T_PLUS = {
- SHARED "map_block_blocked_ports_map",
SHARED "map_clatd_clat_egress4_map",
SHARED "map_clatd_clat_ingress6_map",
SHARED "map_dscpPolicy_ipv4_dscp_policies_map",
SHARED "map_dscpPolicy_ipv6_dscp_policies_map",
SHARED "map_dscpPolicy_socket_policy_cache_map",
- SHARED "map_gentle_test",
- SHARED "prog_gentle_skfilter_accept",
NETD "map_netd_app_uid_stats_map",
+ NETD "map_netd_blocked_ports_map",
NETD "map_netd_configuration_map",
NETD "map_netd_cookie_tag_map",
NETD "map_netd_data_saver_enabled_map",
@@ -126,8 +119,13 @@
// Provided by *current* mainline module for T+ devices with 5.4+ kernels
static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
- NETD_RO "prog_block_bind4_block_port",
- NETD_RO "prog_block_bind6_block_port",
+ NETD "prog_netd_bind4_inet4_bind",
+ NETD "prog_netd_bind6_inet6_bind",
+};
+
+// Provided by *current* mainline module for T+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_T_5_10_PLUS = {
+ NETD "prog_netd_cgroupsockrelease_inet_release",
};
// Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -161,11 +159,6 @@
NETD "prog_netd_setsockopt_prog",
};
-// Provided by *current* mainline module for U+ devices with 5.10+ kernels
-static const set<string> MAINLINE_FOR_V_5_10_PLUS = {
- NETD "prog_netd_cgroupsockrelease_inet_release",
-};
-
static void addAll(set<string>& a, const set<string>& b) {
a.insert(b.begin(), b.end());
}
@@ -196,7 +189,6 @@
// S requires Linux Kernel 4.9+ and thus requires eBPF support.
if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
- DO_EXPECT(IsAtLeastS() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_S_5_10_PLUS);
// Nothing added or removed in SCv2.
@@ -204,6 +196,7 @@
DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
+ DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
// U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
@@ -215,7 +208,6 @@
if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
- DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_V_5_10_PLUS);
for (const auto& file : mustExist) {
EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
deleted file mode 100644
index 353525e..0000000
--- a/bpf_progs/block.c
+++ /dev/null
@@ -1,79 +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.
- */
-
-#include <linux/types.h>
-#include <linux/bpf.h>
-#include <netinet/in.h>
-#include <stdint.h>
-
-// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
-
-#include "bpf_helpers.h"
-
-static const int ALLOW = 1;
-static const int DISALLOW = 0;
-
-DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
- 1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
-
-static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
- if (!ctx->user_port) return ALLOW;
-
- switch (ctx->protocol) {
- case IPPROTO_TCP:
- case IPPROTO_MPTCP:
- case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
- case IPPROTO_DCCP:
- case IPPROTO_SCTP:
- break;
- default:
- return ALLOW; // unknown protocols are allowed
- }
-
- int key = ctx->user_port >> 6;
- int shift = ctx->user_port & 63;
-
- uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
- // Lookup should never fail in reality, but if it does return here to keep the
- // BPF verifier happy.
- if (!val) return ALLOW;
-
- if ((*val >> shift) & 1) return DISALLOW;
- return ALLOW;
-}
-
-// the program need to be accessible/loadable by netd (from netd updatable plugin)
-#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
- "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-
-DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
- return block_port(ctx);
-}
-
-DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
- return block_port(ctx);
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("ConnectivityNative");
-DISABLE_BTF_ON_USER_BUILDS();
-DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
deleted file mode 100644
index 8bb8ad5..0000000
--- a/bpf_progs/dscpPolicy.c
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <linux/bpf.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <linux/types.h>
-#include <netinet/in.h>
-#include <netinet/udp.h>
-#include <stdint.h>
-#include <string.h>
-
-// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
-
-#include "bpf_helpers.h"
-#include "dscpPolicy.h"
-
-#define ECN_MASK 3
-#define IP4_OFFSET(field, header) ((header) + offsetof(struct iphdr, field))
-#define UPDATE_TOS(dscp, tos) ((dscp) << 2) | ((tos) & ECN_MASK)
-
-DEFINE_BPF_MAP_GRW(socket_policy_cache_map, HASH, uint64_t, RuleEntry, CACHE_MAP_SIZE, AID_SYSTEM)
-
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
-
-static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4) {
- void* data = (void*)(long)skb->data;
- const void* data_end = (void*)(long)skb->data_end;
-
- const int l2_header_size = sizeof(struct ethhdr);
- struct ethhdr* eth = data;
-
- if (data + l2_header_size > data_end) return;
-
- int hdr_size = 0;
-
- // used for map lookup
- uint64_t cookie = bpf_get_socket_cookie(skb);
- if (!cookie) return;
-
- __be16 sport = 0;
- uint16_t dport = 0;
- uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
- struct in6_addr src_ip = {};
- struct in6_addr dst_ip = {};
- uint8_t tos = 0; // Only used for IPv4
- __be32 old_first_be32 = 0; // Only used for IPv6
- if (ipv4) {
- const struct iphdr* const iph = (void*)(eth + 1);
- hdr_size = l2_header_size + sizeof(struct iphdr);
- // Must have ipv4 header
- if (data + hdr_size > data_end) return;
-
- // IP version must be 4
- if (iph->version != 4) return;
-
- // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
- if (iph->ihl != 5) return;
-
- // V4 mapped address in in6_addr sets 10/11 position to 0xff.
- src_ip.s6_addr32[2] = htonl(0x0000ffff);
- dst_ip.s6_addr32[2] = htonl(0x0000ffff);
-
- // Copy IPv4 address into in6_addr for easy comparison below.
- src_ip.s6_addr32[3] = iph->saddr;
- dst_ip.s6_addr32[3] = iph->daddr;
- protocol = iph->protocol;
- tos = iph->tos;
- } else {
- struct ipv6hdr* ip6h = (void*)(eth + 1);
- hdr_size = l2_header_size + sizeof(struct ipv6hdr);
- // Must have ipv6 header
- if (data + hdr_size > data_end) return;
-
- if (ip6h->version != 6) return;
-
- src_ip = ip6h->saddr;
- dst_ip = ip6h->daddr;
- protocol = ip6h->nexthdr;
- old_first_be32 = *(__be32*)ip6h;
- }
-
- switch (protocol) {
- case IPPROTO_UDP:
- case IPPROTO_UDPLITE: {
- struct udphdr* udp;
- udp = data + hdr_size;
- if ((void*)(udp + 1) > data_end) return;
- sport = udp->source;
- dport = ntohs(udp->dest);
- } break;
- case IPPROTO_TCP: {
- struct tcphdr* tcp;
- tcp = data + hdr_size;
- if ((void*)(tcp + 1) > data_end) return;
- sport = tcp->source;
- dport = ntohs(tcp->dest);
- } break;
- default:
- return;
- }
-
- RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cookie);
-
- if (existing_rule &&
- v6_equal(src_ip, existing_rule->src_ip) &&
- v6_equal(dst_ip, existing_rule->dst_ip) &&
- skb->ifindex == existing_rule->ifindex &&
- sport == existing_rule->src_port &&
- dport == existing_rule->dst_port &&
- protocol == existing_rule->proto) {
- if (existing_rule->dscp_val < 0) return;
- if (ipv4) {
- uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
- bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
- sizeof(uint16_t));
- bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
- } else {
- __be32 new_first_be32 =
- htonl(ntohl(old_first_be32) & 0xF03FFFFF | (existing_rule->dscp_val << 22));
- bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
- BPF_F_RECOMPUTE_CSUM);
- }
- return;
- }
-
- // Linear scan ipv4_dscp_policies_map since no stored params match skb.
- int best_score = 0;
- int8_t new_dscp = -1;
-
- for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
- // Using a uint64 in for loop prevents infinite loop during BPF load,
- // but the key is uint32, so convert back.
- uint32_t key = i;
-
- DscpPolicy* policy;
- if (ipv4) {
- policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
- } else {
- policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
- }
-
- // If the policy lookup failed, just continue (this should not ever happen)
- if (!policy) continue;
-
- // If policy iface index does not match skb, then skip to next policy.
- if (policy->ifindex != skb->ifindex) continue;
-
- int score = 0;
-
- if (policy->present_fields & PROTO_MASK_FLAG) {
- if (protocol != policy->proto) continue;
- score += 0xFFFF;
- }
- if (policy->present_fields & SRC_IP_MASK_FLAG) {
- if (v6_not_equal(src_ip, policy->src_ip)) continue;
- score += 0xFFFF;
- }
- if (policy->present_fields & DST_IP_MASK_FLAG) {
- if (v6_not_equal(dst_ip, policy->dst_ip)) continue;
- score += 0xFFFF;
- }
- if (policy->present_fields & SRC_PORT_MASK_FLAG) {
- if (sport != policy->src_port) continue;
- score += 0xFFFF;
- }
- if (dport < policy->dst_port_start) continue;
- if (dport > policy->dst_port_end) continue;
- score += 0xFFFF + policy->dst_port_start - policy->dst_port_end;
-
- if (score > best_score) {
- best_score = score;
- new_dscp = policy->dscp_val;
- }
- }
-
- RuleEntry value = {
- .src_ip = src_ip,
- .dst_ip = dst_ip,
- .ifindex = skb->ifindex,
- .src_port = sport,
- .dst_port = dport,
- .proto = protocol,
- .dscp_val = new_dscp,
- };
-
- // Update cache with found policy.
- bpf_socket_policy_cache_map_update_elem(&cookie, &value, BPF_ANY);
-
- if (new_dscp < 0) return;
-
- // Need to store bytes after updating map or program will not load.
- if (ipv4) {
- uint8_t new_tos = UPDATE_TOS(new_dscp, tos);
- bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
- bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
- } else {
- __be32 new_first_be32 = htonl(ntohl(old_first_be32) & 0xF03FFFFF | (new_dscp << 22));
- bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
- BPF_F_RECOMPUTE_CSUM);
- }
- return;
-}
-
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
- KVER_5_15)
-(struct __sk_buff* skb) {
- if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
-
- if (skb->protocol == htons(ETH_P_IP)) {
- match_policy(skb, true);
- } else if (skb->protocol == htons(ETH_P_IPV6)) {
- match_policy(skb, false);
- }
-
- // Always return TC_ACT_PIPE
- return TC_ACT_PIPE;
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
-DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/gentle.c b/bpf_progs/gentle.c
deleted file mode 100644
index ab9620e..0000000
--- a/bpf_progs/gentle.c
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-
-#include "bpf_helpers.h"
-#include "bpf_net_helpers.h"
-
-DEFINE_BPF_MAP_GRW(test, ARRAY, int, uint64_t, 1, AID_SYSTEM)
-
-DEFINE_BPF_PROG("skfilter/accept", AID_ROOT, AID_SYSTEM, accept)
-(struct __sk_buff *skb) {
- return 1;
-}
-
-LICENSE("Apache 2.0");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 21be1d3..39ff2d4 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -22,6 +22,16 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+java_aconfig_library {
+ name: "com.android.net.flags-aconfig-java",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
aconfig_declarations {
name: "com.android.net.thread.flags-aconfig",
package: "com.android.net.thread.flags",
@@ -30,6 +40,16 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+java_aconfig_library {
+ name: "com.android.net.thread.flags-aconfig-java",
+ aconfig_declarations: "com.android.net.thread.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
aconfig_declarations {
name: "nearby_flags",
package: "com.android.nearby.flags",
@@ -37,3 +57,30 @@
srcs: ["nearby_flags.aconfig"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+
+java_aconfig_library {
+ name: "com.android.nearby.flags-aconfig-java",
+ aconfig_declarations: "nearby_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
+aconfig_declarations {
+ name: "com.android.networksecurity.flags-aconfig",
+ package: "com.android.net.ct.flags",
+ container: "com.android.tethering",
+ srcs: ["networksecurity_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_aconfig_library {
+ name: "networksecurity_flags_java_lib",
+ aconfig_declarations: "com.android.networksecurity.flags-aconfig",
+ min_sdk_version: "30",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ apex_available: ["com.android.tethering"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/OWNERS b/common/OWNERS
index e7f5d11..989d286 100644
--- a/common/OWNERS
+++ b/common/OWNERS
@@ -1 +1,2 @@
per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
+per-file networksecurity_flags.aconfig = file:platform/packages/modules/Connectivity:main:/networksecurity/OWNERS
\ No newline at end of file
diff --git a/common/flags.aconfig b/common/flags.aconfig
index bc4168b..4c6d8ba 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -10,6 +10,7 @@
namespace: "android_core_networking"
description: "Set data saver through ConnectivityManager API"
bug: "297836825"
+ is_fixed_read_only: true
}
flag {
@@ -18,6 +19,7 @@
namespace: "android_core_networking"
description: "This flag controls whether isUidNetworkingBlocked is supported"
bug: "297836825"
+ is_fixed_read_only: true
}
flag {
@@ -26,6 +28,7 @@
namespace: "android_core_networking"
description: "Block network access for apps in a low importance background state"
bug: "304347838"
+ is_fixed_read_only: true
}
flag {
@@ -34,6 +37,7 @@
namespace: "android_core_networking_ipsec"
description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
bug: "308011229"
+ is_fixed_read_only: true
}
flag {
@@ -42,6 +46,7 @@
namespace: "android_core_networking"
description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
bug: "216524590"
+ is_fixed_read_only: true
}
flag {
@@ -50,6 +55,7 @@
namespace: "android_core_networking"
description: "Flag for API to support requesting restricted wifi"
bug: "315835605"
+ is_fixed_read_only: true
}
flag {
@@ -58,6 +64,7 @@
namespace: "android_core_networking"
description: "Flag for local network capability API"
bug: "313000440"
+ is_fixed_read_only: true
}
flag {
@@ -66,6 +73,7 @@
namespace: "android_core_networking"
description: "Flag for satellite transport API"
bug: "320514105"
+ is_fixed_read_only: true
}
flag {
@@ -74,6 +82,7 @@
namespace: "android_core_networking"
description: "Flag for API to support nsd subtypes"
bug: "265095929"
+ is_fixed_read_only: true
}
flag {
@@ -82,6 +91,7 @@
namespace: "android_core_networking"
description: "Flag for API to register nsd offload engine"
bug: "301713539"
+ is_fixed_read_only: true
}
flag {
@@ -90,6 +100,7 @@
namespace: "android_core_networking"
description: "Flag for metered network firewall chain API"
bug: "332628891"
+ is_fixed_read_only: true
}
flag {
@@ -98,6 +109,7 @@
namespace: "android_core_networking"
description: "Flag for oem deny chains blocked reasons API"
bug: "328732146"
+ is_fixed_read_only: true
}
flag {
@@ -106,6 +118,7 @@
namespace: "android_core_networking"
description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
bug: "339559837"
+ is_fixed_read_only: true
}
flag {
@@ -114,4 +127,32 @@
namespace: "android_core_networking"
description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
bug: "343823469"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "tethering_request_virtual"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for introducing TETHERING_VIRTUAL type"
+ bug: "340376953"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "netstats_add_entries"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for NetworkStats#addEntries API"
+ bug: "335680025"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "tethering_active_sessions_metrics"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for collecting tethering active sessions metrics"
+ bug: "354619988"
+ is_fixed_read_only: true
}
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
new file mode 100644
index 0000000..6438ba4
--- /dev/null
+++ b/common/networksecurity_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.net.ct.flags"
+container: "com.android.tethering"
+flag {
+ name: "certificate_transparency_service"
+ is_exported: true
+ namespace: "network_security"
+ description: "Enable service for certificate transparency log list data"
+ bug: "319829948"
+ is_fixed_read_only: true
+}
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 43acd1b..14b70d0 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -8,3 +8,30 @@
description: "Controls whether the Android Thread feature is enabled"
bug: "301473012"
}
+
+flag {
+ name: "configuration_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread configuration is enabled"
+ bug: "342519412"
+}
+
+flag {
+ name: "channel_max_powers_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread setting max power of channel feature is enabled"
+ bug: "346686506"
+}
+
+flag {
+ name: "epskc_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread Ephemeral Key feature is enabled"
+ bug: "348323500"
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index bc919ac..7551b92 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -62,12 +62,14 @@
static_libs: [
// Cannot go to framework-connectivity because mid_sdk checks require 31.
"modules-utils-binary-xml",
+ "com.android.nearby.flags-aconfig-java",
+ "com.android.net.thread.flags-aconfig-java",
],
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
- "framework-bluetooth",
- "framework-wifi",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
// Compile against the entire implementation of framework-connectivity,
// including hidden methods. This is safe because if framework-connectivity-t is
// on the bootclasspath (i.e., T), then framework-connectivity is also on the
@@ -97,26 +99,12 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
-// The filegroup lists files that are necessary for verifying building mdns as a standalone,
-// for use with service-connectivity-mdns-standalone-build-test
-// This filegroup should never be included in anywhere in the module build. It is only used for
-// building service-connectivity-mdns-standalone-build-test target. The files will be renamed by
-// copybara to prevent them from being shadowed by the bootclasspath copies.
-filegroup {
- name: "framework-connectivity-t-mdns-standalone-build-sources",
- srcs: [
- "src/android/net/nsd/OffloadEngine.java",
- "src/android/net/nsd/OffloadServiceInfo.java",
- ],
- visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
-
java_library {
name: "framework-connectivity-t-pre-jarjar",
defaults: ["framework-connectivity-t-defaults"],
libs: [
- "framework-bluetooth",
- "framework-wifi",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-location.stubs.module_lib",
],
@@ -137,6 +125,12 @@
// framework-connectivity-pre-jarjar match at runtime.
jarjar_rules: ":framework-connectivity-jarjar-rules",
stub_only_libs: [
+ // static_libs is not used to compile stubs. So libs which have
+ // been included in static_libs might still need to
+ // be in stub_only_libs to be usable when generating the API stubs.
+ "com.android.net.flags-aconfig-java",
+ "com.android.nearby.flags-aconfig-java",
+ "com.android.net.thread.flags-aconfig-java",
// Use prebuilt framework-connectivity stubs to avoid circular dependencies
"sdk_module-lib_current_framework-connectivity",
],
@@ -201,6 +195,9 @@
"com.android.net.thread.flags-aconfig",
"nearby_flags",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// This rule is not used anymore(b/268440216).
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 1f1953c..09a3681 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -310,6 +310,7 @@
public final class NetworkStats implements java.lang.Iterable<android.net.NetworkStats.Entry> android.os.Parcelable {
ctor public NetworkStats(long, int);
method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
+ method @FlaggedApi("com.android.net.flags.netstats_add_entries") @NonNull public android.net.NetworkStats addEntries(@NonNull java.util.List<android.net.NetworkStats.Entry>);
method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
method public android.net.NetworkStats clone();
method public int describeContents();
@@ -497,16 +498,30 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
}
+ @FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
+ method public int describeContents();
+ method public boolean isDhcpv6PdEnabled();
+ method public boolean isNat64Enabled();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
+ }
+
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void activateEphemeralKeyMode(@NonNull java.time.Duration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void deactivateEphemeralKeyMode(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @NonNull public java.time.Duration getMaxEphemeralKeyLifetime();
method public int getThreadVersion();
method public static boolean isAttached(int);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @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 leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void registerConfigurationCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
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 @FlaggedApi("com.android.net.thread.flags.channel_max_powers_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setChannelMaxPowers(@NonNull @Size(min=1) android.util.SparseIntArray, @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 @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
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
@@ -514,6 +529,10 @@
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 @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_DISABLED = 0; // 0x0
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_ENABLED = 1; // 0x1
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_IN_USE = 2; // 0x2
+ field public static final int MAX_POWER_CHANNEL_DISABLED = -2147483648; // 0x80000000
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
@@ -527,6 +546,7 @@
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public default void onEphemeralKeyStateChanged(int, @Nullable String, @Nullable java.time.Instant);
method public default void onPartitionIdChanged(long);
method public default void onThreadEnableStateChanged(int);
}
@@ -546,6 +566,7 @@
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
+ field public static final int ERROR_UNSUPPORTED_FEATURE = 13; // 0xd
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
diff --git a/framework-t/lint-baseline.xml b/framework-t/lint-baseline.xml
new file mode 100644
index 0000000..4e206ed
--- /dev/null
+++ b/framework-t/lint-baseline.xml
@@ -0,0 +1,1313 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+ <issue
+ id="FlaggedApi"
+ message="Field `SERVICE_NAME` is a flagged API and should be inside an `if (ThreadNetworkFlags.threadEnabled())` check (or annotate the surrounding method `registerServiceWrappers` with `@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" ThreadNetworkManager.SERVICE_NAME,"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java"
+ line="101"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Class `ThreadNetworkManager` is a flagged API and should be inside an `if (ThreadNetworkFlags.threadEnabled())` check (or annotate the surrounding method `registerServiceWrappers` with `@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" ThreadNetworkManager.class,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java"
+ line="102"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `ThreadNetworkManager()` is a flagged API and should be inside an `if (ThreadNetworkFlags.threadEnabled())` check (or annotate the surrounding method `registerServiceWrappers` with `@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" return new ThreadNetworkManager(context, managerService);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java"
+ line="106"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="87"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="87"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="87"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="529"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="573"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="605"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="This is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `handleMessage` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" final String s = getNsdServiceInfoType((DiscoveryRequest) obj);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1076"
+ column="61"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `getServiceType()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `getNsdServiceInfoType` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" return r.getServiceType();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1236"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `Builder()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1477"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `build()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)"
+ errorLine2=" ^">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1477"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `setNetwork()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)"
+ errorLine2=" ^">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1477"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `discoverServices()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" discoverServices(request, executor, listener);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1479"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `Builder()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" new DiscoveryRequest.Builder(protocolType, serviceType).build();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1566"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `build()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" new DiscoveryRequest.Builder(protocolType, serviceType).build();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+ line="1566"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `getSubtypes()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `NsdServiceInfo` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" mSubtypes = new ArraySet<>(other.getSubtypes());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdServiceInfo.java"
+ line="106"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `setSubtypes()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `createFromParcel` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" info.setSubtypes(new ArraySet<>(in.createStringArrayList()));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdServiceInfo.java"
+ line="673"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+</issues>
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
index 7f0c1fe..01ac106 100644
--- a/framework-t/src/android/net/INetworkStatsService.aidl
+++ b/framework-t/src/android/net/INetworkStatsService.aidl
@@ -78,13 +78,16 @@
void unregisterUsageRequest(in DataUsageRequest request);
/** Get the uid stats information since boot */
- long getUidStats(int uid, int type);
+ NetworkStats getTypelessUidStats(int uid);
/** Get the iface stats information since boot */
- long getIfaceStats(String iface, int type);
+ NetworkStats getTypelessIfaceStats(String iface);
/** Get the total network stats information since boot */
- long getTotalStats(int type);
+ NetworkStats getTypelessTotalStats();
+
+ /** Get the uid stats information (with specified type) since boot */
+ long getUidStats(int uid, int type);
/** Registers a network stats provider */
INetworkStatsProviderCallback registerNetworkStatsProvider(String tag,
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3f74e1c..39e2b5b 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,13 +65,6 @@
public class IpSecManager {
private static final String TAG = "IpSecManager";
- // 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 IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
- }
-
/**
* Feature flag to declare the kernel support of updating IPsec SAs.
*
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 70c9bc8..35bd008 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,7 +15,6 @@
*/
package android.net;
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
import static android.net.IpSecManager.INVALID_RESOURCE_ID;
import android.annotation.CallbackExecutor;
@@ -35,6 +34,7 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
import dalvik.system.CloseGuard;
@@ -220,7 +220,7 @@
* occurs.
* @see IpSecTransformState
*/
- @FlaggedApi(IPSEC_TRANSFORM_STATE)
+ @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
public void requestIpSecTransformState(
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index 5b80ae2..b6628ee 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -15,8 +15,6 @@
*/
package android.net;
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
-
import static com.android.internal.annotations.VisibleForTesting.Visibility;
import android.annotation.FlaggedApi;
@@ -26,6 +24,7 @@
import android.os.SystemClock;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
import com.android.net.module.util.HexDump;
import java.util.Objects;
@@ -39,7 +38,7 @@
* IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
* numbers, callers can estimate IPsec data loss in the inbound direction.
*/
-@FlaggedApi(IPSEC_TRANSFORM_STATE)
+@FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
public final class IpSecTransformState implements Parcelable {
private final long mTimestamp;
private final long mTxHighestSequenceNumber;
@@ -197,7 +196,7 @@
* <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)
+ @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
public static final class Builder {
private long mTimestamp;
private long mTxHighestSequenceNumber;
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index e9a3f58..a2c4fc3 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -18,6 +18,7 @@
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,6 +34,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import libcore.util.EmptyArray;
@@ -845,6 +847,21 @@
}
/**
+ * Adds multiple entries to a copy of this NetworkStats instance.
+ *
+ * @param entries The entries to add.
+ * @return A new NetworkStats instance with the added entries.
+ */
+ @FlaggedApi(Flags.FLAG_NETSTATS_ADD_ENTRIES)
+ public @NonNull NetworkStats addEntries(@NonNull final List<Entry> entries) {
+ final NetworkStats newStats = this.clone();
+ for (final Entry entry : Objects.requireNonNull(entries)) {
+ newStats.combineValues(entry);
+ }
+ return newStats;
+ }
+
+ /**
* Add given values with an existing row, or create a new row if
* {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can
* also be used to subtract values from existing rows.
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 77c8001..3b6a69b 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -40,6 +40,9 @@
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
+import java.util.Iterator;
+import java.util.Objects;
+
/**
* Class that provides network traffic statistics. These statistics include
@@ -730,11 +733,7 @@
* @return The number of transmitted packets.
*/
public static long getTxPackets(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_TX_PACKETS);
}
/**
@@ -753,11 +752,7 @@
* @return The number of received packets.
*/
public static long getRxPackets(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_RX_PACKETS);
}
/**
@@ -776,11 +771,7 @@
* @return The number of transmitted bytes.
*/
public static long getTxBytes(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_TX_BYTES);
}
/**
@@ -799,51 +790,31 @@
* @return The number of received bytes.
*/
public static long getRxBytes(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_RX_BYTES);
}
/** {@hide} */
@TestApi
public static long getLoopbackTxPackets() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
}
/** {@hide} */
@TestApi
public static long getLoopbackRxPackets() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
}
/** {@hide} */
@TestApi
public static long getLoopbackTxBytes() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
}
/** {@hide} */
@TestApi
public static long getLoopbackRxBytes() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
}
/**
@@ -856,11 +827,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxPackets() {
- try {
- return getStatsService().getTotalStats(TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_TX_PACKETS);
}
/**
@@ -873,11 +840,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxPackets() {
- try {
- return getStatsService().getTotalStats(TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_RX_PACKETS);
}
/**
@@ -890,11 +853,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxBytes() {
- try {
- return getStatsService().getTotalStats(TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_TX_BYTES);
}
/**
@@ -907,11 +866,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxBytes() {
- try {
- return getStatsService().getTotalStats(TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_RX_BYTES);
}
/**
@@ -933,11 +888,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidTxBytes(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_TX_BYTES);
}
/**
@@ -959,11 +910,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidRxBytes(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_RX_BYTES);
}
/**
@@ -985,11 +932,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidTxPackets(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_TX_PACKETS);
}
/**
@@ -1011,11 +954,50 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidRxPackets(int uid) {
+ return getUidStats(uid, TYPE_RX_PACKETS);
+ }
+
+ /** @hide */
+ public static long getUidStats(int uid, int type) {
+ if (!isEntryValueTypeValid(type)
+ || android.os.Process.myUid() != uid) {
+ return UNSUPPORTED;
+ }
+ final NetworkStats stats;
try {
- return getStatsService().getUidStats(uid, TYPE_RX_PACKETS);
+ stats = getStatsService().getTypelessUidStats(uid);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
+ return getValueForTypeFromFirstEntry(stats, type);
+ }
+
+ /** @hide */
+ public static long getTotalStats(int type) {
+ if (!isEntryValueTypeValid(type)) {
+ return UNSUPPORTED;
+ }
+ final NetworkStats stats;
+ try {
+ stats = getStatsService().getTypelessTotalStats();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return getValueForTypeFromFirstEntry(stats, type);
+ }
+
+ /** @hide */
+ public static long getIfaceStats(String iface, int type) {
+ if (!isEntryValueTypeValid(type)) {
+ return UNSUPPORTED;
+ }
+ final NetworkStats stats;
+ try {
+ stats = getStatsService().getTypelessIfaceStats(iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return getValueForTypeFromFirstEntry(stats, type);
}
/**
@@ -1143,4 +1125,45 @@
public static final int TYPE_TX_BYTES = 2;
/** {@hide} */
public static final int TYPE_TX_PACKETS = 3;
+
+ /** @hide */
+ private static long getEntryValueForType(@NonNull NetworkStats.Entry entry, int type) {
+ Objects.requireNonNull(entry);
+ if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+ switch (type) {
+ case TYPE_RX_BYTES:
+ return entry.getRxBytes();
+ case TYPE_RX_PACKETS:
+ return entry.getRxPackets();
+ case TYPE_TX_BYTES:
+ return entry.getTxBytes();
+ case TYPE_TX_PACKETS:
+ return entry.getTxPackets();
+ default:
+ throw new IllegalStateException("Bug: Invalid type: "
+ + type + " should not reach here.");
+ }
+ }
+
+ /** @hide */
+ private static boolean isEntryValueTypeValid(int type) {
+ switch (type) {
+ case TYPE_RX_BYTES:
+ case TYPE_RX_PACKETS:
+ case TYPE_TX_BYTES:
+ case TYPE_TX_PACKETS:
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ /** @hide */
+ public static long getValueForTypeFromFirstEntry(@NonNull NetworkStats stats, int type) {
+ Objects.requireNonNull(stats);
+ Iterator<NetworkStats.Entry> iter = stats.iterator();
+ if (!iter.hasNext()) return UNSUPPORTED;
+ return getEntryValueForType(iter.next(), type);
+ }
}
+
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
index b0b71ea..b344943 100644
--- a/framework-t/src/android/net/nsd/DiscoveryRequest.java
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -24,12 +24,14 @@
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.net.flags.Flags;
+
import java.util.Objects;
/**
* Encapsulates parameters for {@link NsdManager#discoverServices}.
*/
-@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+@FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
public final class DiscoveryRequest implements Parcelable {
private final int mProtocolType;
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index b21e22a..116bea6 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -52,6 +52,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import java.lang.annotation.Retention;
@@ -148,22 +149,6 @@
private static final String TAG = NsdManager.class.getSimpleName();
private static final boolean DBG = false;
- // 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 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
@@ -451,7 +436,7 @@
*
* @hide
*/
- @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+ @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
@RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
NETWORK_STACK})
@@ -489,7 +474,7 @@
*
* @hide
*/
- @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+ @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
@RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
NETWORK_STACK})
@@ -1506,7 +1491,7 @@
* @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)
+ @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
@NonNull Executor executor, @NonNull DiscoveryListener listener) {
int key = putListener(listener, executor, discoveryRequest);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 2f675a9..18c59d9 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -30,6 +30,7 @@
import android.util.ArraySet;
import android.util.Log;
+import com.android.net.flags.Flags;
import com.android.net.module.util.InetAddressUtils;
import java.io.UnsupportedEncodingException;
@@ -180,8 +181,18 @@
return new ArrayList<>(mHostAddresses);
}
- /** Set the host addresses */
+ /**
+ * Set the host addresses.
+ *
+ * <p>When registering hosts/services, there can only be one registration including address
+ * records for a given hostname.
+ *
+ * <p>For example, if a client registers a service with the hostname "MyHost" and the address
+ * records of 192.168.1.1 and 192.168.1.2, then other registrations for the hostname "MyHost"
+ * must not have any address record, otherwise there will be a conflict.
+ */
public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+ // TODO: b/284905335 - Notify the client when there is a conflict.
mHostAddresses.clear();
mHostAddresses.addAll(addresses);
}
@@ -517,7 +528,7 @@
* 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)
+ @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
public void setSubtypes(@NonNull Set<String> subtypes) {
mSubtypes.clear();
mSubtypes.addAll(subtypes);
@@ -530,7 +541,7 @@
* NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
* service.
*/
- @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
@NonNull
public Set<String> getSubtypes() {
return Collections.unmodifiableSet(mSubtypes);
diff --git a/framework-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
index 9015985..06655fa 100644
--- a/framework-t/src/android/net/nsd/OffloadEngine.java
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -21,6 +21,8 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import com.android.net.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -34,7 +36,7 @@
*
* @hide
*/
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
public interface OffloadEngine {
/**
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index 98dc83a..e4b2f43 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -26,6 +26,7 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.net.flags.Flags;
import com.android.net.module.util.HexDump;
import java.util.Arrays;
@@ -40,7 +41,7 @@
*
* @hide
*/
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public final class OffloadServiceInfo implements Parcelable {
diff --git a/framework/Android.bp b/framework/Android.bp
index deb1c5a..0334e11 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -63,6 +63,7 @@
":framework-connectivity-sources",
":net-utils-framework-common-srcs",
":framework-connectivity-api-shared-srcs",
+ ":framework-networksecurity-sources",
],
aidl: {
generate_get_transaction_name: true,
@@ -86,17 +87,18 @@
"framework-wifi.stubs.module_lib",
],
static_libs: [
+ "com.android.net.flags-aconfig-java",
// Not using the latest stable version because all functions in the latest version of
// mdns_aidl_interface are deprecated.
"mdns_aidl_interface-V1-java",
"modules-utils-backgroundthread",
"modules-utils-build",
"modules-utils-preconditions",
+ "networksecurity_flags_java_lib",
"framework-connectivity-javastream-protos",
],
impl_only_static_libs: [
- "net-utils-device-common-bpf",
- "net-utils-device-common-struct-base",
+ "net-utils-framework-connectivity",
],
libs: [
"androidx.annotation_annotation",
@@ -109,22 +111,20 @@
],
}
+// Library to allow Cronet to use hidden APIs
java_library {
- name: "framework-connectivity-pre-jarjar",
+ name: "framework-connectivity-pre-jarjar-without-cronet",
defaults: [
"framework-connectivity-defaults",
],
static_libs: [
- "httpclient_api",
- "httpclient_impl",
// Framework-connectivity-pre-jarjar is identical to framework-connectivity
// implementation, but without the jarjar rules. However, framework-connectivity
// is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
// to generate the SDK stubs.
// Even if the library is included in "impl_only_static_libs" of defaults. This is still
// needed because java_library which doesn't understand "impl_only_static_libs".
- "net-utils-device-common-bpf",
- "net-utils-device-common-struct-base",
+ "net-utils-framework-connectivity",
],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
@@ -135,6 +135,21 @@
"framework-tethering.impl",
"framework-wifi.stubs.module_lib",
],
+ visibility: ["//external/cronet:__subpackages__"],
+}
+
+java_library {
+ name: "framework-connectivity-pre-jarjar",
+ defaults: ["framework-module-defaults"],
+ min_sdk_version: "30",
+ static_libs: [
+ "framework-connectivity-pre-jarjar-without-cronet",
+ "httpclient_api",
+ "httpclient_impl",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
@@ -163,8 +178,10 @@
// In preparation for future move
"//packages/modules/Connectivity/apex",
"//packages/modules/Connectivity/framework-t",
+ "//packages/modules/Connectivity/remoteauth/service",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/staticlibs",
"//frameworks/base",
// Tests using hidden APIs
@@ -186,6 +203,7 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
+ "//packages/modules/NetworkStack",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
@@ -194,6 +212,7 @@
},
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "com.android.networksecurity.flags-aconfig",
],
}
@@ -294,6 +313,7 @@
srcs: [
":framework-connectivity-sources",
":framework-connectivity-tiramisu-updatable-sources",
+ ":framework-networksecurity-sources",
":framework-nearby-java-sources",
":framework-thread-sources",
],
@@ -332,7 +352,6 @@
srcs: [
// Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.S)
// or above as appropriate so that API checks are enforced for R+ users of this library
- "src/android/net/RoutingCoordinatorManager.java",
"src/android/net/connectivity/ConnectivityInternalApiUtil.java",
],
visibility: [
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
index 2c0b15f..dddabef 100644
--- a/framework/lint-baseline.xml
+++ b/framework/lint-baseline.xml
@@ -375,4 +375,279 @@
column="34"/>
</issue>
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_BACKGROUND` is a flagged API and should be inside an `if (Flags.basicBackgroundRestrictionsEnabled())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" FIREWALL_CHAIN_BACKGROUND"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+ line="115"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" FIREWALL_CHAIN_METERED_ALLOW"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+ line="137"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" FIREWALL_CHAIN_METERED_DENY_USER,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+ line="146"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_DENY_ADMIN` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" FIREWALL_CHAIN_METERED_DENY_ADMIN"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+ line="147"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_BACKGROUND` is a flagged API and should be inside an `if (Flags.basicBackgroundRestrictionsEnabled())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" case FIREWALL_CHAIN_BACKGROUND:"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+ line="133"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" case FIREWALL_CHAIN_METERED_ALLOW:"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+ line="143"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" case FIREWALL_CHAIN_METERED_DENY_USER:"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+ line="145"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_DENY_ADMIN` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" case FIREWALL_CHAIN_METERED_DENY_ADMIN:"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+ line="147"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `BLOCKED_REASON_APP_BACKGROUND` is a flagged API and should be inside an `if (Flags.basicBackgroundRestrictionsEnabled())` check (or annotate the surrounding method `getUidNetworkingBlockedReasons` with `@FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED) to transfer requirement to caller`)"
+ errorLine1=" blockedReasons |= BLOCKED_REASON_APP_BACKGROUND;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+ line="293"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `BLOCKED_REASON_OEM_DENY` is a flagged API and should be inside an `if (Flags.blockedReasonOemDenyChains())` check (or annotate the surrounding method `getUidNetworkingBlockedReasons` with `@FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" blockedReasons |= BLOCKED_REASON_OEM_DENY;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+ line="296"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `addUidToMeteredNetworkAllowList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_ALLOW);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="6191"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `removeUidFromMeteredNetworkAllowList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_DENY);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="6214"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `addUidToMeteredNetworkDenyList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_DENY);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="6243"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `removeUidFromMeteredNetworkDenyList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+ errorLine1=" mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_ALLOW);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="6273"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+ errorLine1=" private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="767"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+ errorLine1=" defaultCapabilities |= (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="818"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+ errorLine1=" (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="849"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `getSubscriptionIds()` is a flagged API and should be inside an `if (Flags.requestRestrictedWifi())` check (or annotate the surrounding method `restrictCapabilitiesForTestNetwork` with `@FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI) to transfer requirement to caller`)"
+ errorLine1=" final Set<Integer> originalSubIds = getSubscriptionIds();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="1254"
+ column="45"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `TRANSPORT_SATELLITE` is a flagged API and should be inside an `if (Flags.supportTransportSatellite())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE) to transfer requirement to caller`)"
+ errorLine1=" public static final int MAX_TRANSPORT = TRANSPORT_SATELLITE;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="1383"
+ column="45"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `TRANSPORT_SATELLITE` is a flagged API and should be inside an `if (Flags.supportTransportSatellite())` check (or annotate the surrounding method `specifierAcceptableForMultipleTransports` with `@FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE) to transfer requirement to caller`)"
+ errorLine1=" == (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="1836"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `NET_CAPABILITY_LOCAL_NETWORK` is a flagged API and should be inside an `if (Flags.netCapabilityLocalNetwork())` check (or annotate the surrounding method `capabilityNameOf` with `@FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK) to transfer requirement to caller`)"
+ errorLine1=" case NET_CAPABILITY_LOCAL_NETWORK: return "LOCAL_NETWORK";"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="2637"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `capabilityNameOf` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+ errorLine1=" case NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED: return "NOT_BANDWIDTH_CONSTRAINED";"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+ line="2638"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `TRANSPORT_SATELLITE` is a flagged API and should be inside an `if (Flags.supportTransportSatellite())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE) to transfer requirement to caller`)"
+ errorLine1=" TRANSPORT_SATELLITE"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java"
+ line="80"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+ errorLine1=" NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="291"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="Method `getSubscriptionIds()` is a flagged API and should be inside an `if (Flags.requestRestrictedWifi())` check (or annotate the surrounding method `getSubscriptionIds` with `@FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI) to transfer requirement to caller`)"
+ errorLine1=" return networkCapabilities.getSubscriptionIds();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="887"
+ column="16"/>
+ </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 282a11e..1760fa7 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -261,6 +261,12 @@
IBpfMap<S32, UidOwnerValue> uidOwnerMap,
IBpfMap<S32, U8> dataSaverEnabledMap
) {
+ // System uids are not blocked by firewall chains, see bpf_progs/netd.c
+ // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
+ if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+ return BLOCKED_REASON_NONE;
+ }
+
final long uidRuleConfig;
final long uidMatch;
try {
@@ -331,12 +337,6 @@
) {
throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
- // System uids are not blocked by firewall chains, see bpf_progs/netd.c
- // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
- if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
- return false;
- }
-
final int blockedReasons = getUidNetworkingBlockedReasons(
uid,
configurationMap,
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 4eaf973..1ebc4a3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -71,23 +71,30 @@
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.LruCache;
import android.util.Range;
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import libcore.net.event.NetworkEventDispatcher;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@@ -119,24 +126,6 @@
private static final String TAG = "ConnectivityManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- // 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 SET_DATA_SAVER_VIA_CM =
- "com.android.net.flags.set_data_saver_via_cm";
- static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
- "com.android.net.flags.support_is_uid_networking_blocked";
- static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
- "com.android.net.flags.basic_background_restrictions_enabled";
- static final String METERED_NETWORK_FIREWALL_CHAINS =
- "com.android.net.flags.metered_network_firewall_chains";
- static final String BLOCKED_REASON_OEM_DENY_CHAINS =
- "com.android.net.flags.blocked_reason_oem_deny_chains";
- static final String BLOCKED_REASON_NETWORK_RESTRICTED =
- "com.android.net.flags.blocked_reason_network_restricted";
- }
-
/**
* A change in network connectivity has occurred. A default connection has either
* been established or lost. The NetworkInfo for the affected network is
@@ -913,7 +902,7 @@
*
* @hide
*/
- @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+ @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
@@ -926,7 +915,7 @@
* @see #FIREWALL_CHAIN_OEM_DENY_3
* @hide
*/
- @FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS)
+ @FlaggedApi(Flags.FLAG_BLOCKED_REASON_OEM_DENY_CHAINS)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
@@ -937,7 +926,7 @@
*
* @hide
*/
- @FlaggedApi(Flags.BLOCKED_REASON_NETWORK_RESTRICTED)
+ @FlaggedApi(Flags.FLAG_BLOCKED_REASON_NETWORK_RESTRICTED)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 1 << 8;
@@ -1046,7 +1035,7 @@
* exempted for specific situations while in the background.
* @hide
*/
- @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+ @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_BACKGROUND = 6;
@@ -1114,7 +1103,7 @@
* @hide
*/
// TODO: Merge this chain with data saver and support setFirewallChainEnabled
- @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+ @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_METERED_ALLOW = 10;
@@ -1133,7 +1122,7 @@
* @hide
*/
// TODO: Support setFirewallChainEnabled to control this chain
- @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+ @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11;
@@ -1152,7 +1141,7 @@
* @hide
*/
// TODO: Support setFirewallChainEnabled to control this chain
- @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+ @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12;
@@ -1231,6 +1220,19 @@
@GuardedBy("mTetheringEventCallbacks")
private TetheringManager mTetheringManager;
+ // Cache of the most recently used NetworkCallback classes (not instances) -> method flags.
+ // 100 is chosen kind arbitrarily as an unlikely number of different types of NetworkCallback
+ // overrides that a process may have, and should generally not be reached (for example, the
+ // system server services.jar has been observed with dexdump to have only 16 when this was
+ // added, and a very large system services app only had 18).
+ // If this number is exceeded, the code will still function correctly, but re-registering
+ // using a network callback class that was used before, but 100+ other classes have been used in
+ // the meantime, will be a bit slower (as slow as the first registration) because
+ // getDeclaredMethodsFlag must re-examine the callback class to determine what methods it
+ // overrides.
+ private static final LruCache<Class<? extends NetworkCallback>, Integer> sMethodFlagsCache =
+ new LruCache<>(100);
+
private final Object mEnabledConnectivityManagerFeaturesLock = new Object();
// mEnabledConnectivityManagerFeatures is lazy-loaded in this ConnectivityManager instance, but
// fetched from ConnectivityService, where it is loaded in ConnectivityService startup, so it
@@ -3996,6 +3998,55 @@
*/
public static class NetworkCallback {
/**
+ * Bitmask of method flags with all flags set.
+ * @hide
+ */
+ public static final int DECLARED_METHODS_ALL = ~0;
+
+ /**
+ * Bitmask of method flags with no flag set.
+ * @hide
+ */
+ public static final int DECLARED_METHODS_NONE = 0;
+
+ // Tracks whether an instance was created via reflection without calling the constructor.
+ private final boolean mConstructorWasCalled;
+
+ /**
+ * Annotation for NetworkCallback methods to verify filtering is configured properly.
+ *
+ * This is only used in tests to ensure that tests fail when a new callback is added, or
+ * callbacks are modified, without updating
+ * {@link NetworkCallbackMethodsHolder#NETWORK_CB_METHODS} properly.
+ * @hide
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ @VisibleForTesting
+ public @interface FilteredCallback {
+ /**
+ * The NetworkCallback.METHOD_* ID of this method.
+ */
+ int methodId();
+
+ /**
+ * The ConnectivityManager.CALLBACK_* message that this method is directly called by.
+ *
+ * If this method is not called by any message, this should be
+ * {@link #CALLBACK_TRANSITIVE_CALLS_ONLY}.
+ */
+ int calledByCallbackId();
+
+ /**
+ * If this method may call other NetworkCallback methods, an array of methods it calls.
+ *
+ * Only direct calls (not transitive calls) should be included. The IDs must be
+ * NetworkCallback.METHOD_* IDs.
+ */
+ int[] mayCall() default {};
+ }
+
+ /**
* No flags associated with this callback.
* @hide
*/
@@ -4058,6 +4109,7 @@
throw new IllegalArgumentException("Invalid flags");
}
mFlags = flags;
+ mConstructorWasCalled = true;
}
/**
@@ -4075,7 +4127,9 @@
*
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONPRECHECK, calledByCallbackId = CALLBACK_PRECHECK)
public void onPreCheck(@NonNull Network network) {}
+ private static final int METHOD_ONPRECHECK = 1;
/**
* Called when the framework connects and has declared a new network ready for use.
@@ -4090,6 +4144,13 @@
* @param blocked Whether access to the {@link Network} is blocked due to system policy.
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
+ calledByCallbackId = CALLBACK_AVAILABLE,
+ // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+ // needs to be updated too.
+ mayCall = { METHOD_ONAVAILABLE_4ARGS,
+ METHOD_ONLOCALNETWORKINFOCHANGED,
+ METHOD_ONBLOCKEDSTATUSCHANGED_INT })
public final void onAvailable(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities,
@NonNull LinkProperties linkProperties,
@@ -4102,6 +4163,7 @@
if (null != localInfo) onLocalNetworkInfoChanged(network, localInfo);
onBlockedStatusChanged(network, blocked);
}
+ private static final int METHOD_ONAVAILABLE_5ARGS = 2;
/**
* Legacy variant of onAvailable that takes a boolean blocked reason.
@@ -4114,6 +4176,15 @@
*
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
+ calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
+ // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+ // needs to be updated too.
+ mayCall = { METHOD_ONAVAILABLE_1ARG,
+ METHOD_ONNETWORKSUSPENDED,
+ METHOD_ONCAPABILITIESCHANGED,
+ METHOD_ONLINKPROPERTIESCHANGED
+ })
public void onAvailable(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities,
@NonNull LinkProperties linkProperties,
@@ -4127,6 +4198,7 @@
onLinkPropertiesChanged(network, linkProperties);
// No call to onBlockedStatusChanged here. That is done by the caller.
}
+ private static final int METHOD_ONAVAILABLE_4ARGS = 3;
/**
* Called when the framework connects and has declared a new network ready for use.
@@ -4157,7 +4229,10 @@
*
* @param network The {@link Network} of the satisfying network.
*/
+ @FilteredCallback(methodId = METHOD_ONAVAILABLE_1ARG,
+ calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY)
public void onAvailable(@NonNull Network network) {}
+ private static final int METHOD_ONAVAILABLE_1ARG = 4;
/**
* Called when the network is about to be lost, typically because there are no outstanding
@@ -4176,7 +4251,9 @@
* connected for graceful handover; note that the network may still
* suffer a hard loss at any time.
*/
+ @FilteredCallback(methodId = METHOD_ONLOSING, calledByCallbackId = CALLBACK_LOSING)
public void onLosing(@NonNull Network network, int maxMsToLive) {}
+ private static final int METHOD_ONLOSING = 5;
/**
* Called when a network disconnects or otherwise no longer satisfies this request or
@@ -4197,7 +4274,9 @@
*
* @param network The {@link Network} lost.
*/
+ @FilteredCallback(methodId = METHOD_ONLOST, calledByCallbackId = CALLBACK_LOST)
public void onLost(@NonNull Network network) {}
+ private static final int METHOD_ONLOST = 6;
/**
* Called if no network is found within the timeout time specified in
@@ -4207,7 +4286,9 @@
* {@link NetworkRequest} will have already been removed and released, as if
* {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
*/
+ @FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL)
public void onUnavailable() {}
+ private static final int METHOD_ONUNAVAILABLE = 7;
/**
* Called when the network corresponding to this request changes capabilities but still
@@ -4224,8 +4305,11 @@
* @param networkCapabilities The new {@link NetworkCapabilities} for this
* network.
*/
+ @FilteredCallback(methodId = METHOD_ONCAPABILITIESCHANGED,
+ calledByCallbackId = CALLBACK_CAP_CHANGED)
public void onCapabilitiesChanged(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities) {}
+ private static final int METHOD_ONCAPABILITIESCHANGED = 8;
/**
* Called when the network corresponding to this request changes {@link LinkProperties}.
@@ -4240,8 +4324,11 @@
* @param network The {@link Network} whose link properties have changed.
* @param linkProperties The new {@link LinkProperties} for this network.
*/
+ @FilteredCallback(methodId = METHOD_ONLINKPROPERTIESCHANGED,
+ calledByCallbackId = CALLBACK_IP_CHANGED)
public void onLinkPropertiesChanged(@NonNull Network network,
@NonNull LinkProperties linkProperties) {}
+ private static final int METHOD_ONLINKPROPERTIESCHANGED = 9;
/**
* Called when there is a change in the {@link LocalNetworkInfo} for this network.
@@ -4253,8 +4340,11 @@
* @param localNetworkInfo the new {@link LocalNetworkInfo} for this network.
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONLOCALNETWORKINFOCHANGED,
+ calledByCallbackId = CALLBACK_LOCAL_NETWORK_INFO_CHANGED)
public void onLocalNetworkInfoChanged(@NonNull Network network,
@NonNull LocalNetworkInfo localNetworkInfo) {}
+ private static final int METHOD_ONLOCALNETWORKINFOCHANGED = 10;
/**
* Called when the network the framework connected to for this request suspends data
@@ -4273,7 +4363,10 @@
*
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONNETWORKSUSPENDED,
+ calledByCallbackId = CALLBACK_SUSPENDED)
public void onNetworkSuspended(@NonNull Network network) {}
+ private static final int METHOD_ONNETWORKSUSPENDED = 11;
/**
* Called when the network the framework connected to for this request
@@ -4287,7 +4380,9 @@
*
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONNETWORKRESUMED, calledByCallbackId = CALLBACK_RESUMED)
public void onNetworkResumed(@NonNull Network network) {}
+ private static final int METHOD_ONNETWORKRESUMED = 12;
/**
* Called when access to the specified network is blocked or unblocked.
@@ -4300,7 +4395,10 @@
* @param network The {@link Network} whose blocked status has changed.
* @param blocked The blocked status of this {@link Network}.
*/
+ @FilteredCallback(methodId = METHOD_ONBLOCKEDSTATUSCHANGED_BOOL,
+ calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY)
public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {}
+ private static final int METHOD_ONBLOCKEDSTATUSCHANGED_BOOL = 13;
/**
* Called when access to the specified network is blocked or unblocked, or the reason for
@@ -4318,10 +4416,14 @@
* @param blocked The blocked status of this {@link Network}.
* @hide
*/
+ @FilteredCallback(methodId = METHOD_ONBLOCKEDSTATUSCHANGED_INT,
+ calledByCallbackId = CALLBACK_BLK_CHANGED,
+ mayCall = { METHOD_ONBLOCKEDSTATUSCHANGED_BOOL })
@SystemApi(client = MODULE_LIBRARIES)
public void onBlockedStatusChanged(@NonNull Network network, @BlockedReason int blocked) {
onBlockedStatusChanged(network, blocked != 0);
}
+ private static final int METHOD_ONBLOCKEDSTATUSCHANGED_INT = 14;
private NetworkRequest networkRequest;
private final int mFlags;
@@ -4349,6 +4451,7 @@
}
}
+ private static final int CALLBACK_TRANSITIVE_CALLS_ONLY = 0;
/** @hide */
public static final int CALLBACK_PRECHECK = 1;
/** @hide */
@@ -4373,10 +4476,13 @@
public static final int CALLBACK_BLK_CHANGED = 11;
/** @hide */
public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+ // When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
+
/** @hide */
public static String getCallbackName(int whichCallback) {
switch (whichCallback) {
+ case CALLBACK_TRANSITIVE_CALLS_ONLY: return "CALLBACK_TRANSITIVE_CALLS_ONLY";
case CALLBACK_PRECHECK: return "CALLBACK_PRECHECK";
case CALLBACK_AVAILABLE: return "CALLBACK_AVAILABLE";
case CALLBACK_LOSING: return "CALLBACK_LOSING";
@@ -4394,6 +4500,68 @@
}
}
+ /** @hide */
+ @VisibleForTesting
+ public static class NetworkCallbackMethod {
+ @NonNull
+ public final String mName;
+ @NonNull
+ public final Class<?>[] mParameterTypes;
+ // Bitmask of CALLBACK_* that may transitively call this method.
+ public final int mCallbacksCallingThisMethod;
+
+ public NetworkCallbackMethod(@NonNull String name, @NonNull Class<?>[] parameterTypes,
+ int callbacksCallingThisMethod) {
+ mName = name;
+ mParameterTypes = parameterTypes;
+ mCallbacksCallingThisMethod = callbacksCallingThisMethod;
+ }
+ }
+
+ // Holder class for the list of NetworkCallbackMethod. This ensures the list is only created
+ // once on first usage, and not just on ConnectivityManager class initialization.
+ /** @hide */
+ @VisibleForTesting
+ public static class NetworkCallbackMethodsHolder {
+ public static final NetworkCallbackMethod[] NETWORK_CB_METHODS =
+ new NetworkCallbackMethod[] {
+ method("onPreCheck", 1 << CALLBACK_PRECHECK, Network.class),
+ // Note the final overload of onAvailable is not included, since it cannot
+ // match any overridden method.
+ method("onAvailable", 1 << CALLBACK_AVAILABLE, Network.class),
+ method("onAvailable", 1 << CALLBACK_AVAILABLE,
+ Network.class, NetworkCapabilities.class,
+ LinkProperties.class, boolean.class),
+ method("onLosing", 1 << CALLBACK_LOSING, Network.class, int.class),
+ method("onLost", 1 << CALLBACK_LOST, Network.class),
+ method("onUnavailable", 1 << CALLBACK_UNAVAIL),
+ method("onCapabilitiesChanged",
+ 1 << CALLBACK_CAP_CHANGED | 1 << CALLBACK_AVAILABLE,
+ Network.class, NetworkCapabilities.class),
+ method("onLinkPropertiesChanged",
+ 1 << CALLBACK_IP_CHANGED | 1 << CALLBACK_AVAILABLE,
+ Network.class, LinkProperties.class),
+ method("onLocalNetworkInfoChanged",
+ 1 << CALLBACK_LOCAL_NETWORK_INFO_CHANGED | 1 << CALLBACK_AVAILABLE,
+ Network.class, LocalNetworkInfo.class),
+ method("onNetworkSuspended",
+ 1 << CALLBACK_SUSPENDED | 1 << CALLBACK_AVAILABLE, Network.class),
+ method("onNetworkResumed",
+ 1 << CALLBACK_RESUMED, Network.class),
+ method("onBlockedStatusChanged",
+ 1 << CALLBACK_BLK_CHANGED | 1 << CALLBACK_AVAILABLE,
+ Network.class, boolean.class),
+ method("onBlockedStatusChanged",
+ 1 << CALLBACK_BLK_CHANGED | 1 << CALLBACK_AVAILABLE,
+ Network.class, int.class),
+ };
+
+ private static NetworkCallbackMethod method(
+ String name, int callbacksCallingThisMethod, Class<?>... args) {
+ return new NetworkCallbackMethod(name, args, callbacksCallingThisMethod);
+ }
+ }
+
private static class CallbackHandler extends Handler {
private static final String TAG = "ConnectivityManager.CallbackHandler";
private static final boolean DBG = false;
@@ -4513,6 +4681,14 @@
if (reqType != TRACK_DEFAULT && reqType != TRACK_SYSTEM_DEFAULT && need == null) {
throw new IllegalArgumentException("null NetworkCapabilities");
}
+
+
+ final boolean useDeclaredMethods = isFeatureEnabled(
+ FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS);
+ // Set all bits if the feature is disabled
+ int declaredMethodsFlag = useDeclaredMethods
+ ? tryGetDeclaredMethodsFlag(callback)
+ : NetworkCallback.DECLARED_METHODS_ALL;
final NetworkRequest request;
final String callingPackageName = mContext.getOpPackageName();
try {
@@ -4529,11 +4705,12 @@
if (reqType == LISTEN) {
request = mService.listenForNetwork(
need, messenger, binder, callbackFlags, callingPackageName,
- getAttributionTag());
+ getAttributionTag(), declaredMethodsFlag);
} else {
request = mService.requestNetwork(
asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
- legacyType, callbackFlags, callingPackageName, getAttributionTag());
+ legacyType, callbackFlags, callingPackageName, getAttributionTag(),
+ declaredMethodsFlag);
}
if (request != null) {
sCallbacks.put(request, callback);
@@ -4548,6 +4725,108 @@
return request;
}
+ private int tryGetDeclaredMethodsFlag(@NonNull NetworkCallback cb) {
+ if (!cb.mConstructorWasCalled) {
+ // Do not use the optimization if the callback was created via reflection or mocking,
+ // as for example with dexmaker-mockito-inline methods will be instrumented without
+ // using subclasses. This does not catch all cases as it is still possible to call the
+ // constructor when creating mocks, but by default constructors are not called in that
+ // case.
+ return NetworkCallback.DECLARED_METHODS_ALL;
+ }
+ try {
+ return getDeclaredMethodsFlag(cb.getClass());
+ } catch (LinkageError e) {
+ // This may happen if some methods reference inaccessible classes in their arguments
+ // (for example b/261807130).
+ Log.w(TAG, "Could not get methods from NetworkCallback class", e);
+ // Fall through
+ } catch (Throwable e) {
+ // Log.wtf would be best but this is in app process, so the TerribleFailureHandler may
+ // have unknown effects, possibly crashing the app (default behavior on eng builds or
+ // if the WTF_IS_FATAL setting is set).
+ Log.e(TAG, "Unexpected error while getting methods from NetworkCallback class", e);
+ // Fall through
+ }
+ return NetworkCallback.DECLARED_METHODS_ALL;
+ }
+
+ private static int getDeclaredMethodsFlag(@NonNull Class<? extends NetworkCallback> clazz) {
+ final Integer cachedFlags = sMethodFlagsCache.get(clazz);
+ // As this is not synchronized, it is possible that this method will calculate the
+ // flags for a given class multiple times, but that is fine. LruCache itself is thread-safe.
+ if (cachedFlags != null) {
+ return cachedFlags;
+ }
+
+ int flag = 0;
+ // This uses getMethods instead of getDeclaredMethods, to make sure that if A overrides B
+ // that overrides NetworkCallback, A.getMethods also returns methods declared by B.
+ for (Method classMethod : clazz.getMethods()) {
+ final Class<?> declaringClass = classMethod.getDeclaringClass();
+ if (declaringClass == NetworkCallback.class) {
+ // The callback is as defined by NetworkCallback and not overridden
+ continue;
+ }
+ if (declaringClass == Object.class) {
+ // Optimization: no need to try to match callbacks for methods declared by Object
+ continue;
+ }
+ flag |= getCallbackIdsCallingThisMethod(classMethod);
+ }
+
+ if (flag == 0) {
+ // dexmaker-mockito-inline (InlineDexmakerMockMaker), for example for mockito-extended,
+ // modifies bytecode of classes in-place to add hooks instead of creating subclasses,
+ // which would not be detected. When no method is found, fall back to enabling callbacks
+ // for all methods.
+ // This will not catch the case where both NetworkCallback bytecode is modified and a
+ // subclass of NetworkCallback that has some overridden methods are used. But this kind
+ // of bytecode injection is only possible in debuggable processes, with a JVMTI debug
+ // agent attached, so it should not cause real issues.
+ // There may be legitimate cases where an empty callback is filed with no method
+ // overridden, for example requestNetwork(requestForCell, new NetworkCallback()) which
+ // would ensure that one cell network stays up. But there is no way to differentiate
+ // such NetworkCallbacks from a mock that called the constructor, so this code will
+ // register the callback with DECLARED_METHODS_ALL and turn off the optimization in that
+ // case. Apps are not expected to do this often anyway since the usefulness is very
+ // limited.
+ flag = NetworkCallback.DECLARED_METHODS_ALL;
+ }
+ sMethodFlagsCache.put(clazz, flag);
+ return flag;
+ }
+
+ /**
+ * Find out which of the base methods in NetworkCallback will call this method.
+ *
+ * For example, in the case of onLinkPropertiesChanged, this will be
+ * (1 << CALLBACK_IP_CHANGED) | (1 << CALLBACK_AVAILABLE).
+ */
+ private static int getCallbackIdsCallingThisMethod(@NonNull Method method) {
+ for (NetworkCallbackMethod baseMethod : NetworkCallbackMethodsHolder.NETWORK_CB_METHODS) {
+ if (!baseMethod.mName.equals(method.getName())) {
+ continue;
+ }
+ Class<?>[] methodParams = method.getParameterTypes();
+
+ // As per JLS 8.4.8.1., a method m1 must have a subsignature of method m2 to override
+ // it. And as per JLS 8.4.2, this means the erasure of the signature of m2 must be the
+ // same as the signature of m1. Since type erasure is done at compile time, with
+ // reflection the erased types are already observed, so the (erased) parameter types
+ // must be equal.
+ // So for example a method that is identical to a NetworkCallback method, except with
+ // one parameter being a subclass of the parameter in the original method, will never
+ // be called since it is not an override (the erasure of the arguments are not the same)
+ // Therefore, the method is an override only if methodParams is exactly equal to
+ // the base method's parameter types.
+ if (Arrays.equals(baseMethod.mParameterTypes, methodParams)) {
+ return baseMethod.mCallbacksCallingThisMethod;
+ }
+ }
+ return 0;
+ }
+
private boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
synchronized (mEnabledConnectivityManagerFeaturesLock) {
if (mEnabledConnectivityManagerFeatures == null) {
@@ -6157,7 +6436,7 @@
* @throws IllegalStateException if failed.
* @hide
*/
- @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+ @FlaggedApi(Flags.FLAG_SET_DATA_SAVER_VIA_CM)
@SystemApi(client = MODULE_LIBRARIES)
@RequiresPermission(anyOf = {
android.Manifest.permission.NETWORK_SETTINGS,
@@ -6418,7 +6697,7 @@
// is provided by linux file group permission AID_NET_BW_ACCT and the
// selinux context fs_bpf_net*.
// Only the system server process and the network stack have access.
- @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+ @FlaggedApi(Flags.FLAG_SUPPORT_IS_UID_NETWORKING_BLOCKED)
@SystemApi(client = MODULE_LIBRARIES)
// Note b/326143935 kernel bug can trigger crash on some T device.
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -6446,21 +6725,40 @@
}
}
- private static final Object sRoutingCoordinatorManagerLock = new Object();
- @GuardedBy("sRoutingCoordinatorManagerLock")
- private static RoutingCoordinatorManager sRoutingCoordinatorManager = null;
/** @hide */
@RequiresApi(Build.VERSION_CODES.S)
- public RoutingCoordinatorManager getRoutingCoordinatorManager() {
+ public IBinder getRoutingCoordinatorService() {
try {
- synchronized (sRoutingCoordinatorManagerLock) {
- if (null == sRoutingCoordinatorManager) {
- sRoutingCoordinatorManager = new RoutingCoordinatorManager(mContext,
- IRoutingCoordinator.Stub.asInterface(
- mService.getRoutingCoordinatorService()));
- }
- return sRoutingCoordinatorManager;
- }
+ return mService.getRoutingCoordinatorService();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the specified ConnectivityService feature status. This method is for test code to check
+ * whether the feature is enabled or not.
+ * Note that tests can not just read DeviceConfig since ConnectivityService reads flag at
+ * startup. For example, it's possible that the current flag value is "disable"(-1) but the
+ * feature is enabled since the flag value was "enable"(1) when ConnectivityService started up.
+ * If the ConnectivityManager needs to check the ConnectivityService feature status for non-test
+ * purpose, define feature in {@link ConnectivityManagerFeature} and use
+ * {@link #isFeatureEnabled} instead.
+ *
+ * @param featureFlag target flag for feature
+ * @return {@code true} if the feature is enabled, {@code false} if the feature is disabled.
+ * @throws IllegalArgumentException if the flag is invalid
+ *
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public boolean isConnectivityServiceFeatureEnabledForTesting(final String featureFlag) {
+ try {
+ return mService.isConnectivityServiceFeatureEnabledForTesting(featureFlag);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index f9de8ed..47b3316 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -153,7 +153,8 @@
NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
in Messenger messenger, int timeoutSec, in IBinder binder, int legacy,
- int callbackFlags, String callingPackageName, String callingAttributionTag);
+ int callbackFlags, String callingPackageName, String callingAttributionTag,
+ int declaredMethodsFlag);
NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities,
in PendingIntent operation, String callingPackageName, String callingAttributionTag);
@@ -162,7 +163,7 @@
NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities,
in Messenger messenger, in IBinder binder, int callbackFlags, String callingPackageName,
- String callingAttributionTag);
+ String callingAttributionTag, int declaredMethodsFlag);
void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
in PendingIntent operation, String callingPackageName,
@@ -261,4 +262,6 @@
IBinder getRoutingCoordinatorService();
long getEnabledConnectivityManagerFeatures();
+
+ boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag);
}
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 6a14bde..4a50397 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -41,6 +41,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.BitUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -124,22 +125,6 @@
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
- // 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 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";
- static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
- "com.android.net.flags.net_capability_not_bandwidth_constrained";
- }
-
/**
* Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
* app permissions.
@@ -761,7 +746,7 @@
* usage on constrained networks, such as disabling network access to apps that are not in the
* foreground.
*/
- @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ @FlaggedApi(Flags.FLAG_NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
@@ -1374,7 +1359,7 @@
/**
* Indicates this network uses a Satellite transport.
*/
- @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+ @FlaggedApi(Flags.FLAG_SUPPORT_TRANSPORT_SATELLITE)
public static final int TRANSPORT_SATELLITE = 10;
/** @hide */
@@ -2864,7 +2849,7 @@
* @return
*/
@NonNull
- @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ @FlaggedApi(Flags.FLAG_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 502ac6f..89572b3 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -50,6 +50,8 @@
import android.text.TextUtils;
import android.util.Range;
+import com.android.net.flags.Flags;
+
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -144,12 +146,6 @@
* 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.
@@ -616,7 +612,7 @@
* @param subIds A {@code Set} that represents subscription IDs.
*/
@NonNull
- @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
mNetworkCapabilities.setSubscriptionIds(subIds);
return this;
@@ -880,7 +876,7 @@
* @return Set of Integer values for this instance.
*/
@NonNull
- @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
public Set<Integer> getSubscriptionIds() {
// No need to make a defensive copy here as NC#getSubscriptionIds() already returns
// a new set.
diff --git a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index 79f1f65..ba39ca0 100644
--- a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -18,15 +18,14 @@
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.RoutingCoordinatorManager;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.RequiresApi;
/**
- * Utility providing limited access to module-internal APIs which are only available on Android T+,
- * as this class is only in the bootclasspath on T+ as part of framework-connectivity.
+ * Utility providing limited access to module-internal APIs which are only available on Android S+,
+ * as this class is only in the bootclasspath on S+ as part of framework-connectivity.
*
* R+ module components like Tethering cannot depend on all hidden symbols from
* framework-connectivity. They only have access to stable API stubs where newer APIs can be
@@ -54,8 +53,8 @@
* @return an instance of the coordinator manager
*/
@RequiresApi(Build.VERSION_CODES.S)
- public static RoutingCoordinatorManager getRoutingCoordinatorManager(Context ctx) {
+ public static IBinder getRoutingCoordinator(Context ctx) {
final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
- return cm.getRoutingCoordinatorManager();
+ return cm.getRoutingCoordinatorService();
}
}
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 4be102c..6bfa54d 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -49,13 +49,17 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
"framework-location.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
+ "com.android.nearby.flags-aconfig-java",
],
visibility: [
"//packages/modules/Connectivity/nearby/tests:__subpackages__",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index cae653d..150394b 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -37,6 +37,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import com.android.nearby.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -84,7 +85,7 @@
* Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
* supported the device.
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
/**
@@ -92,7 +93,7 @@
* #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")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
/**
@@ -100,7 +101,7 @@
* #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")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
/**
@@ -122,9 +123,12 @@
private static final int POWERED_OFF_FINDING_EID_LENGTH = 20;
- private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY =
+ private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY_RO =
"ro.bluetooth.finder.supported";
+ private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY_PERSIST =
+ "persist.bluetooth.finder.supported";
+
/**
* TODO(b/286137024): Remove this when CTS R5 is rolled out.
* Whether allows Fast Pair to scan.
@@ -526,7 +530,7 @@
*
* @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
Objects.requireNonNull(eids);
@@ -570,7 +574,7 @@
* @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")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
Preconditions.checkArgument(
@@ -602,7 +606,7 @@
* #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
* #setPoweredOffFindingMode(int)}
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public @PoweredOffFindingMode int getPoweredOffFindingMode() {
if (!isPoweredOffFindingSupported()) {
@@ -617,7 +621,9 @@
}
private boolean isPoweredOffFindingSupported() {
- return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY));
+ return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY_RO))
+ || Boolean.parseBoolean(SystemProperties.get(
+ POWER_OFF_FINDING_SUPPORTED_PROPERTY_PERSIST));
}
private boolean areLocationAndBluetoothEnabled() {
diff --git a/nearby/framework/lint-baseline.xml b/nearby/framework/lint-baseline.xml
new file mode 100644
index 0000000..e1081ee
--- /dev/null
+++ b/nearby/framework/lint-baseline.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="87"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="87"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="87"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="95"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="103"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="529"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="573"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1=" @FlaggedApi("com.android.nearby.flags.powered_off_finding")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+ line="605"
+ column="17"/>
+ </issue>
+
+</issues>
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 749113d..c9c7b44 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -35,11 +35,11 @@
],
libs: [
"androidx.annotation_annotation",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
"error_prone_annotations",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-t.impl",
- "framework-statsd",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"androidx.core_core",
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 8009303..9d42dd1 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -30,9 +30,9 @@
"truth",
],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
"framework-bluetooth.stubs.module_lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-t.impl",
"framework-location.stubs.module_lib",
],
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index 9e1ec70..472f4f0 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -21,7 +21,6 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner"/>
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 2950568..4d2d1d5 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -27,9 +27,9 @@
srcs: ["src/**/*.java"],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
],
compile_multilib: "both",
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
deleted file mode 100644
index e9c6d8a..0000000
--- a/netbpfload/NetBpfLoad.cpp
+++ /dev/null
@@ -1,521 +0,0 @@
-/*
- * Copyright (C) 2017-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.
- */
-
-#ifndef LOG_TAG
-#define LOG_TAG "NetBpfLoad"
-#endif
-
-#include <arpa/inet.h>
-#include <dirent.h>
-#include <elf.h>
-#include <error.h>
-#include <fcntl.h>
-#include <inttypes.h>
-#include <linux/bpf.h>
-#include <linux/unistd.h>
-#include <net/if.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include <sys/mman.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <android/api-level.h>
-#include <android-base/logging.h>
-#include <android-base/macros.h>
-#include <android-base/properties.h>
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-#include <log/log.h>
-
-#include "BpfSyscallWrappers.h"
-#include "bpf/BpfUtils.h"
-#include "loader.h"
-
-namespace android {
-namespace bpf {
-
-using base::StartsWith;
-using base::EndsWith;
-using std::string;
-
-static bool exists(const char* const path) {
- int v = access(path, F_OK);
- if (!v) return true;
- if (errno == ENOENT) return false;
- ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
- abort(); // can only hit this if permissions (likely selinux) are screwed up
-}
-
-
-const Location locations[] = {
- // S+ Tethering mainline module (network_stack): tether offload
- {
- .dir = "/apex/com.android.tethering/etc/bpf/",
- .prefix = "tethering/",
- },
- // T+ Tethering mainline module (shared with netd & system server)
- // netutils_wrapper (for iptables xt_bpf) has access to programs
- {
- .dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
- .prefix = "netd_shared/",
- },
- // T+ Tethering mainline module (shared with netd & system server)
- // netutils_wrapper has no access, netd has read only access
- {
- .dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
- .prefix = "netd_readonly/",
- },
- // T+ Tethering mainline module (shared with system server)
- {
- .dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
- .prefix = "net_shared/",
- },
- // T+ Tethering mainline module (not shared, just network_stack)
- {
- .dir = "/apex/com.android.tethering/etc/bpf/net_private/",
- .prefix = "net_private/",
- },
-};
-
-static int loadAllElfObjects(const unsigned int bpfloader_ver, const Location& location) {
- int retVal = 0;
- DIR* dir;
- struct dirent* ent;
-
- if ((dir = opendir(location.dir)) != NULL) {
- while ((ent = readdir(dir)) != NULL) {
- string s = ent->d_name;
- if (!EndsWith(s, ".o")) continue;
-
- string progPath(location.dir);
- progPath += s;
-
- bool critical;
- int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
- if (ret) {
- if (critical) retVal = ret;
- ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
- } else {
- ALOGD("Loaded object: %s", progPath.c_str());
- }
- }
- closedir(dir);
- }
- return retVal;
-}
-
-static int createSysFsBpfSubDir(const char* const prefix) {
- if (*prefix) {
- mode_t prevUmask = umask(0);
-
- string s = "/sys/fs/bpf/";
- s += prefix;
-
- errno = 0;
- int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
- if (ret && errno != EEXIST) {
- const int err = errno;
- ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
- return -err;
- }
-
- umask(prevUmask);
- }
- return 0;
-}
-
-// Technically 'value' doesn't need to be newline terminated, but it's best
-// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
-// which is usually how kernel devs test the actual sysctl interfaces.
-static int writeProcSysFile(const char *filename, const char *value) {
- base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
- if (fd < 0) {
- const int err = errno;
- ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
- return -err;
- }
- int len = strlen(value);
- int v = write(fd, value, len);
- if (v < 0) {
- const int err = errno;
- ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
- return -err;
- }
- if (v != len) {
- // In practice, due to us only using this for /proc/sys/... files, this can't happen.
- ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
- return -EINVAL;
- }
- return 0;
-}
-
-#define APEX_MOUNT_POINT "/apex/com.android.tethering"
-const char * const platformBpfLoader = "/system/bin/bpfloader";
-
-static 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;
- ALOGV("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;
-}
-
-static bool hasGSM() {
- static string ph = base::GetProperty("gsm.current.phone-type", "");
- static bool gsm = (ph != "");
- static bool logged = false;
- if (!logged) {
- logged = true;
- ALOGI("hasGSM(gsm.current.phone-type='%s'): %s", ph.c_str(), gsm ? "true" : "false");
- }
- return gsm;
-}
-
-static bool isTV() {
- if (hasGSM()) return false; // TVs don't do GSM
-
- static string key = base::GetProperty("ro.oem.key1", "");
- static bool tv = StartsWith(key, "ATV00");
- static bool logged = false;
- if (!logged) {
- logged = true;
- ALOGI("isTV(ro.oem.key1='%s'): %s.", key.c_str(), tv ? "true" : "false");
- }
- return tv;
-}
-
-static int doLoad(char** argv, char * const envp[]) {
- const bool runningAsRoot = !getuid(); // true iff U QPR3 or V+
-
- // Any released device will have codename REL instead of a 'real' codename.
- // For safety: default to 'REL' so we default to unreleased=false on failure.
- const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
-
- // goog/main device_api_level is bumped *way* before aosp/main api level
- // (the latter only gets bumped during the push of goog/main to aosp/main)
- //
- // Since we develop in AOSP, we want it to behave as if it was bumped too.
- //
- // Note that AOSP doesn't really have a good api level (for example during
- // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
- // One could argue that for our purposes AOSP api level should be infinite or 10000.
- //
- // This could also cause api to be increased in goog/main or other branches,
- // but I can't imagine a case where this would be a problem: the problem
- // is rather a too low api level, rather than some ill defined high value.
- // For example as I write this aosp is 34/U, and goog is 35/V,
- // we want to treat both goog & aosp as 35/V, but it's harmless if we
- // treat goog as 36 because that value isn't yet defined to mean anything,
- // and we thus never compare against it.
- //
- // Also note that 'android_get_device_api_level()' is what the
- // //system/core/init/apex_init_util.cpp
- // apex init .XXrc parsing code uses for XX filtering.
- //
- // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
- // but could (should?) perhaps be adjusted to match this.
- const int effective_api_level = android_get_device_api_level() + (int)unreleased;
- const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
- const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
- const bool isAtLeastV = (effective_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");
-
- // Version of Network BpfLoader depends on the Android OS version
- unsigned int bpfloader_ver = 42u; // [42] BPFLOADER_MAINLINE_VERSION
- if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
- if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
- if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
- if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
-
- ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
- bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
- kernelVersion(), describeArch(), getuid(),
- has_platform_bpfloader_rc, has_platform_netbpfload_rc);
-
- if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
- ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
- return 1;
- }
-
- if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
- ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
- return 1;
- }
-
- logTetheringApexVersion();
-
- if (!isAtLeastT) {
- ALOGE("Impossible - not reachable on Android <T.");
- return 1;
- }
-
- // both S and T require kernel 4.9 (and eBpf support)
- if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
- ALOGE("Android T requires kernel 4.9.");
- return 1;
- }
-
- // U bumps the kernel requirement up to 4.14
- if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
- ALOGE("Android U requires kernel 4.14.");
- return 1;
- }
-
- // V bumps the kernel requirement up to 4.19
- // see also: //system/netd/tests/kernel_test.cpp TestKernel419
- if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
- ALOGE("Android V requires kernel 4.19.");
- return 1;
- }
-
- // Technically already required by U, but only enforce on V+
- // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
- if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
- ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
- if (!isTV()) return 1;
- }
-
- // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
- if (isAtLeastV && isKernel32Bit() && isX86()) {
- ALOGE("Android V requires X86 kernel to be 64-bit.");
- if (!isTV()) return 1;
- }
-
- if (isAtLeastV) {
- bool bad = false;
-
- if (!isLtsKernel()) {
- ALOGW("Android V only supports LTS kernels.");
- bad = true;
- }
-
-#define REQUIRE(maj, min, sub) \
- if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
- ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
- bad = true; \
- }
-
- REQUIRE(4, 19, 236)
- REQUIRE(5, 4, 186)
- REQUIRE(5, 10, 199)
- REQUIRE(5, 15, 136)
- REQUIRE(6, 1, 57)
- REQUIRE(6, 6, 0)
-
-#undef REQUIRE
-
- if (bad) {
- ALOGE("Unsupported kernel version (%07x).", kernelVersion());
- }
- }
-
- if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
- /* Android 14/U should only launch on 64-bit kernels
- * T launches on 5.10/5.15
- * U launches on 5.15/6.1
- * So >=5.16 implies isKernel64Bit()
- *
- * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
- *
- * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
- * we also require 64-bit userspace.
- *
- * There are various known issues with 32-bit userspace talking to various
- * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
- * Some of these have userspace or kernel workarounds/hacks.
- * Some of them don't...
- * We're going to be removing the hacks.
- * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
- * Note: this check/enforcement only applies to *system* userspace code,
- * it does not affect unprivileged apps, the 32-on-64 compatibility
- * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
- *
- * Additionally the 32-bit kernel jit support is poor,
- * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
- */
- ALOGE("64-bit userspace required on 6.2+ kernels.");
- if (!isTV()) return 1;
- }
-
- // Ensure we can determine the Android build type.
- if (!isEng() && !isUser() && !isUserdebug()) {
- ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
- getBuildType().c_str());
- return 1;
- }
-
- if (runningAsRoot) {
- // Note: writing this proc file requires being root (always the case on V+)
-
- // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
- // but we need 0 (enabled)
- // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
- // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
- if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
- isAtLeastKernelVersion(5, 13, 0)) return 1;
- }
-
- if (isAtLeastU) {
- // Note: writing these proc files requires CAP_NET_ADMIN
- // and sepolicy which is only present on U+,
- // on Android T and earlier versions they're written from the 'load_bpf_programs'
- // trigger (ie. by init itself) instead.
-
- // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
- // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_BPF_JIT=y)
- // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
- // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
-
- // Enable JIT kallsyms export for privileged users only
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
- }
-
- // Create all the pin subdirectories
- // (this must be done first to allow selinux_context and pin_subdir functionality,
- // which could otherwise fail with ENOENT during object pinning or renaming,
- // due to ordering issues)
- for (const auto& location : locations) {
- 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(bpfloader_ver, location) != 0) {
- ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
- ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
- ALOGE("If this triggers randomly, you might be hitting some memory allocation "
- "problems or startup script race.");
- ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
- sleep(20);
- return 2;
- }
- }
-
- int key = 1;
- int value = 123;
- base::unique_fd map(
- createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
- if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
- ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
- return 1;
- }
-
- // leave a flag that we're done
- if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
-
- // platform bpfloader will only succeed when run as root
- if (!runningAsRoot) {
- // unreachable on U QPR3+ which always runs netbpfload as root
-
- ALOGI("mainline done, no need to transfer control to platform bpf loader.");
- return 0;
- }
-
- // unreachable before U QPR3
- ALOGI("done, transferring control to platform bpfloader.");
-
- // platform BpfLoader *needs* to run as root
- const char * args[] = { platformBpfLoader, NULL, };
- execve(args[0], (char**)args, envp);
- ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
- return 1;
-}
-
-} // namespace bpf
-} // namespace android
-
-int main(int argc, char** argv, char * const envp[]) {
- android::base::InitLogging(argv, &android::base::KernelLogger);
-
- if (argc == 2 && !strcmp(argv[1], "done")) {
- // we're being re-exec'ed from platform bpfloader to 'finalize' things
- if (!android::base::SetProperty("bpf.progs_loaded", "1")) {
- ALOGE("Failed to set bpf.progs_loaded property to 1.");
- return 125;
- }
- ALOGI("success.");
- return 0;
- }
-
- return android::bpf::doLoad(argv, envp);
-}
diff --git a/netbpfload/initrc-doc/README.txt b/netbpfload/initrc-doc/README.txt
deleted file mode 100644
index 42e1fc2..0000000
--- a/netbpfload/initrc-doc/README.txt
+++ /dev/null
@@ -1,62 +0,0 @@
-This directory contains comment stripped versions of
- //system/bpf/bpfloader/bpfloader.rc
-from previous versions of Android.
-
-Generated via:
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
-
-this is entirely equivalent to:
- (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
-
-it is also equivalent to:
- (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
-
-ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
-
-Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
-
-
-Key takeaways:
-
-= R bpfloader:
- - CHOWN + SYS_ADMIN
- - asynchronous startup
- - platform only
- - proc file setup handled by initrc
-
-= S bpfloader
- - adds NET_ADMIN
- - synchronous startup
- - platform + mainline tethering offload
-
-= T bpfloader
- - platform + mainline networking (including tethering offload)
- - supported btf for maps via exec of btfloader
-
-= U bpfloader
- - proc file setup moved into bpfloader binary
- - explicitly specified user and groups:
- group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
- user root
-
-= U QPR2 bpfloader
- - drops support of btf for maps
- - invocation of /system/bin/netbpfload binary, which after handling *all*
- networking bpf related things executes the platform /system/bin/bpfloader
- which handles non-networking bpf.
-
-Note that there is now a copy of 'netbpfload' provided by the tethering apex
-mainline module at /apex/com.android.tethering/bin/netbpfload, which due
-to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
-added for btf map support (specifically the ability to exec the "btfloader").
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
deleted file mode 100644
index bb7c56f..0000000
--- a/netbpfload/loader.cpp
+++ /dev/null
@@ -1,1203 +0,0 @@
-/*
- * Copyright (C) 2018-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.
- */
-
-#define LOG_TAG "NetBpfLoad"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/bpf.h>
-#include <linux/elf.h>
-#include <log/log.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <sys/stat.h>
-#include <sys/utsname.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include "BpfSyscallWrappers.h"
-#include "bpf/BpfUtils.h"
-#include "bpf/bpf_map_def.h"
-#include "loader.h"
-
-#include <cstdlib>
-#include <fstream>
-#include <iostream>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include <android-base/cmsg.h>
-#include <android-base/file.h>
-#include <android-base/properties.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-
-#define BPF_FS_PATH "/sys/fs/bpf/"
-
-// Size of the BPF log buffer for verifier logging
-#define BPF_LOAD_LOG_SZ 0xfffff
-
-// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
-#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
-
-using android::base::StartsWith;
-using android::base::unique_fd;
-using std::ifstream;
-using std::ios;
-using std::optional;
-using std::string;
-using std::vector;
-
-namespace android {
-namespace bpf {
-
-const std::string& getBuildType() {
- static std::string t = android::base::GetProperty("ro.build.type", "unknown");
- return t;
-}
-
-static unsigned int page_size = static_cast<unsigned int>(getpagesize());
-
-constexpr const char* lookupSelinuxContext(const domain d, const char* const unspecified = "") {
- switch (d) {
- case domain::unspecified: return unspecified;
- case domain::tethering: return "fs_bpf_tethering";
- case domain::net_private: return "fs_bpf_net_private";
- case domain::net_shared: return "fs_bpf_net_shared";
- case domain::netd_readonly: return "fs_bpf_netd_readonly";
- case domain::netd_shared: return "fs_bpf_netd_shared";
- default: return "(unrecognized)";
- }
-}
-
-domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
- for (domain d : AllDomains) {
- // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
- if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
- if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
- }
- ALOGW("ignoring unrecognized selinux_context '%-32s'", s);
- // We should return 'unrecognized' here, however: returning unspecified will
- // result in the system simply using the default context, which in turn
- // will allow future expansion by adding more restrictive selinux types.
- // Older bpfloader will simply ignore that, and use the less restrictive default.
- // This does mean you CANNOT later add a *less* restrictive type than the default.
- //
- // Note: we cannot just abort() here as this might be a mainline module shipped optional update
- return domain::unspecified;
-}
-
-constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
- switch (d) {
- case domain::unspecified: return unspecified;
- case domain::tethering: return "tethering/";
- case domain::net_private: return "net_private/";
- case domain::net_shared: return "net_shared/";
- case domain::netd_readonly: return "netd_readonly/";
- case domain::netd_shared: return "netd_shared/";
- default: return "(unrecognized)";
- }
-};
-
-domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
- for (domain d : AllDomains) {
- // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
- if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
- if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
- }
- ALOGE("unrecognized pin_subdir '%-32s'", s);
- // pin_subdir affects the object's full pathname,
- // and thus using the default would change the location and thus our code's ability to find it,
- // hence this seems worth treating as a true error condition.
- //
- // Note: we cannot just abort() here as this might be a mainline module shipped optional update
- // However, our callers will treat this as an error, and stop loading the specific .o,
- // which will fail bpfloader if the .o is marked critical.
- return domain::unrecognized;
-}
-
-static string pathToObjName(const string& path) {
- // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
- string filename = android::base::Split(path, "/").back();
- // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
- string name = filename.substr(0, filename.find_last_of('.'));
- // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
- // this can be used to provide duplicate programs (mux based on the bpfloader version)
- return name.substr(0, name.find_last_of('@'));
-}
-
-typedef struct {
- const char* name;
- enum bpf_prog_type type;
- enum bpf_attach_type expected_attach_type;
-} sectionType;
-
-/*
- * Map section name prefixes to program types, the section name will be:
- * SECTION(<prefix>/<name-of-program>)
- * For example:
- * SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
- * is the name of the program, and tracepoint is the type.
- *
- * However, be aware that you should not be directly using the SECTION() macro.
- * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
- *
- * Programs shipped inside the tethering apex should be limited to networking stuff,
- * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
- * since they are less stable abi/api and may conflict with platform uses of bpf.
- */
-sectionType sectionNameTypes[] = {
- {"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
- {"bind6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
- {"cgroupskb/", BPF_PROG_TYPE_CGROUP_SKB, BPF_ATTACH_TYPE_UNSPEC},
- {"cgroupsock/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_ATTACH_TYPE_UNSPEC},
- {"cgroupsockcreate/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_CREATE},
- {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_RELEASE},
- {"connect4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
- {"connect6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
- {"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
- {"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
- {"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
- {"lwt_in/", BPF_PROG_TYPE_LWT_IN, BPF_ATTACH_TYPE_UNSPEC},
- {"lwt_out/", BPF_PROG_TYPE_LWT_OUT, BPF_ATTACH_TYPE_UNSPEC},
- {"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_ATTACH_TYPE_UNSPEC},
- {"lwt_xmit/", BPF_PROG_TYPE_LWT_XMIT, BPF_ATTACH_TYPE_UNSPEC},
- {"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
- {"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
- {"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
- {"recvmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
- {"schedact/", BPF_PROG_TYPE_SCHED_ACT, BPF_ATTACH_TYPE_UNSPEC},
- {"schedcls/", BPF_PROG_TYPE_SCHED_CLS, BPF_ATTACH_TYPE_UNSPEC},
- {"sendmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
- {"sendmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
- {"setsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT},
- {"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER, BPF_ATTACH_TYPE_UNSPEC},
- {"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
- {"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
- {"xdp/", BPF_PROG_TYPE_XDP, BPF_ATTACH_TYPE_UNSPEC},
-};
-
-typedef struct {
- enum bpf_prog_type type;
- enum bpf_attach_type expected_attach_type;
- string name;
- vector<char> data;
- vector<char> rel_data;
- optional<struct bpf_prog_def> prog_def;
-
- unique_fd prog_fd; /* fd after loading */
-} codeSection;
-
-static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
- elfFile.seekg(0);
- if (elfFile.fail()) return -1;
-
- if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
-
- return 0;
-}
-
-/* Reads all section header tables into an Shdr array */
-static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
- Elf64_Ehdr eh;
- int ret = 0;
-
- ret = readElfHeader(elfFile, &eh);
- if (ret) return ret;
-
- elfFile.seekg(eh.e_shoff);
- if (elfFile.fail()) return -1;
-
- /* Read shdr table entries */
- shTable.resize(eh.e_shnum);
-
- if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
-
- return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
- vector<Elf64_Shdr> shTable;
- int ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- elfFile.seekg(shTable[id].sh_offset);
- if (elfFile.fail()) return -1;
-
- sec.resize(shTable[id].sh_size);
- if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
-
- return 0;
-}
-
-/* Read whole section header string table */
-static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
- Elf64_Ehdr eh;
- int ret = readElfHeader(elfFile, &eh);
- if (ret) return ret;
-
- ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
- if (ret) return ret;
-
- return 0;
-}
-
-/* Get name from offset in strtab */
-static int getSymName(ifstream& elfFile, int nameOff, string& name) {
- int ret;
- vector<char> secStrTab;
-
- ret = readSectionHeaderStrtab(elfFile, secStrTab);
- if (ret) return ret;
-
- if (nameOff >= (int)secStrTab.size()) return -1;
-
- name = string((char*)secStrTab.data() + nameOff);
- return 0;
-}
-
-/* Reads a full section by name - example to get the GPL license */
-static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
- vector<char> secStrTab;
- vector<Elf64_Shdr> shTable;
- int ret;
-
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- ret = readSectionHeaderStrtab(elfFile, secStrTab);
- if (ret) return ret;
-
- for (int i = 0; i < (int)shTable.size(); i++) {
- char* secname = secStrTab.data() + shTable[i].sh_name;
- if (!secname) continue;
-
- if (!strcmp(secname, name)) {
- vector<char> dataTmp;
- dataTmp.resize(shTable[i].sh_size);
-
- elfFile.seekg(shTable[i].sh_offset);
- if (elfFile.fail()) return -1;
-
- if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
- data = dataTmp;
- return 0;
- }
- }
- return -2;
-}
-
-unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
- vector<char> theBytes;
- int ret = readSectionByName(name, elfFile, theBytes);
- if (ret) {
- ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
- return defVal;
- } else if (theBytes.size() < sizeof(unsigned int)) {
- ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
- return defVal;
- } else {
- // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
- unsigned int value = static_cast<unsigned char>(theBytes[3]);
- value <<= 8;
- value += static_cast<unsigned char>(theBytes[2]);
- value <<= 8;
- value += static_cast<unsigned char>(theBytes[1]);
- value <<= 8;
- value += static_cast<unsigned char>(theBytes[0]);
- ALOGI("Section %s value is %u [0x%x]", name, value, value);
- return value;
- }
-}
-
-static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
- int ret;
- vector<Elf64_Shdr> shTable;
-
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- for (int i = 0; i < (int)shTable.size(); i++) {
- if ((int)shTable[i].sh_type != type) continue;
-
- vector<char> dataTmp;
- dataTmp.resize(shTable[i].sh_size);
-
- elfFile.seekg(shTable[i].sh_offset);
- if (elfFile.fail()) return -1;
-
- if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
- data = dataTmp;
- return 0;
- }
- return -2;
-}
-
-static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
- return (a.st_value < b.st_value);
-}
-
-static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
- int ret, numElems;
- Elf64_Sym* buf;
- vector<char> secData;
-
- ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
- if (ret) return ret;
-
- buf = (Elf64_Sym*)secData.data();
- numElems = (secData.size() / sizeof(Elf64_Sym));
- data.assign(buf, buf + numElems);
-
- if (sort) std::sort(data.begin(), data.end(), symCompare);
- return 0;
-}
-
-static enum bpf_prog_type getSectionType(string& name) {
- for (auto& snt : sectionNameTypes)
- if (StartsWith(name, snt.name)) return snt.type;
-
- return BPF_PROG_TYPE_UNSPEC;
-}
-
-static enum bpf_attach_type getExpectedAttachType(string& name) {
- for (auto& snt : sectionNameTypes)
- if (StartsWith(name, snt.name)) return snt.expected_attach_type;
- return BPF_ATTACH_TYPE_UNSPEC;
-}
-
-/*
-static string getSectionName(enum bpf_prog_type type)
-{
- for (auto& snt : sectionNameTypes)
- if (snt.type == type)
- return string(snt.name);
-
- return "UNKNOWN SECTION NAME " + std::to_string(type);
-}
-*/
-
-static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
- size_t sizeOfBpfProgDef) {
- vector<char> pdData;
- int ret = readSectionByName("progs", elfFile, pdData);
- if (ret) return ret;
-
- if (pdData.size() % sizeOfBpfProgDef) {
- ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
- pdData.size(), sizeOfBpfProgDef);
- return -1;
- };
-
- int progCount = pdData.size() / sizeOfBpfProgDef;
- pd.resize(progCount);
- size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
-
- const char* dataPtr = pdData.data();
- for (auto& p : pd) {
- // First we zero initialize
- memset(&p, 0, sizeof(p));
- // Then we set non-zero defaults
- p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER; // v1.0
- // Then we copy over the structure prefix from the ELF file.
- memcpy(&p, dataPtr, trimmedSize);
- // Move to next struct in the ELF file
- dataPtr += sizeOfBpfProgDef;
- }
- return 0;
-}
-
-static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
- optional<unsigned> symbolType = std::nullopt) {
- int ret;
- string name;
- vector<Elf64_Sym> symtab;
- vector<Elf64_Shdr> shTable;
-
- ret = readSymTab(elfFile, 1 /* sort */, symtab);
- if (ret) return ret;
-
- /* Get index of section */
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- int sec_idx = -1;
- for (int i = 0; i < (int)shTable.size(); i++) {
- ret = getSymName(elfFile, shTable[i].sh_name, name);
- if (ret) return ret;
-
- if (!name.compare(sectionName)) {
- sec_idx = i;
- break;
- }
- }
-
- /* No section found with matching name*/
- if (sec_idx == -1) {
- ALOGW("No %s section could be found in elf object", sectionName.c_str());
- return -1;
- }
-
- for (int i = 0; i < (int)symtab.size(); i++) {
- if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
-
- if (symtab[i].st_shndx == sec_idx) {
- string s;
- ret = getSymName(elfFile, symtab[i].st_name, s);
- if (ret) return ret;
- names.push_back(s);
- }
- }
-
- return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
- vector<Elf64_Shdr> shTable;
- int entries, ret = 0;
-
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
- entries = shTable.size();
-
- vector<struct bpf_prog_def> pd;
- ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
- if (ret) return ret;
- vector<string> progDefNames;
- ret = getSectionSymNames(elfFile, "progs", progDefNames);
- if (!pd.empty() && ret) return ret;
-
- for (int i = 0; i < entries; i++) {
- string name;
- codeSection cs_temp;
- cs_temp.type = BPF_PROG_TYPE_UNSPEC;
-
- ret = getSymName(elfFile, shTable[i].sh_name, name);
- if (ret) return ret;
-
- enum bpf_prog_type ptype = getSectionType(name);
-
- if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
-
- // This must be done before '/' is replaced with '_'.
- cs_temp.expected_attach_type = getExpectedAttachType(name);
-
- string oldName = name;
-
- // convert all slashes to underscores
- std::replace(name.begin(), name.end(), '/', '_');
-
- cs_temp.type = ptype;
- cs_temp.name = name;
-
- ret = readSectionByIdx(elfFile, i, cs_temp.data);
- if (ret) return ret;
- ALOGV("Loaded code section %d (%s)", i, name.c_str());
-
- vector<string> csSymNames;
- ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
- if (ret || !csSymNames.size()) return ret;
- for (size_t i = 0; i < progDefNames.size(); ++i) {
- if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
- cs_temp.prog_def = pd[i];
- break;
- }
- }
-
- /* Check for rel section */
- if (cs_temp.data.size() > 0 && i < entries) {
- ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
- if (ret) return ret;
-
- if (name == (".rel" + oldName)) {
- ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
- if (ret) return ret;
- ALOGV("Loaded relo section %d (%s)", i, name.c_str());
- }
- }
-
- if (cs_temp.data.size() > 0) {
- cs.push_back(std::move(cs_temp));
- ALOGV("Adding section %d to cs list", i);
- }
- }
- return 0;
-}
-
-static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
- vector<Elf64_Sym> symtab;
- int ret = 0;
-
- ret = readSymTab(elfFile, 0 /* !sort */, symtab);
- if (ret) return ret;
-
- if (index >= (int)symtab.size()) return -1;
-
- return getSymName(elfFile, symtab[index].st_name, name);
-}
-
-static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
- const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
- // bpfGetFd... family of functions require at minimum a 4.14 kernel,
- // so on 4.9-T kernels just pretend the map matches our expectations.
- // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
- // This is because the primary failure mode we're trying to detect here
- // is either a source code misconfiguration (which is likely kernel independent)
- // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
- if (!isAtLeastKernelVersion(4, 14, 0)) return true;
-
- // Assuming fd is a valid Bpf Map file descriptor then
- // all the following should always succeed on a 4.14+ kernel.
- // If they somehow do fail, they'll return -1 (and set errno),
- // which should then cause (among others) a key_size mismatch.
- int fd_type = bpfGetFdMapType(fd);
- int fd_key_size = bpfGetFdKeySize(fd);
- int fd_value_size = bpfGetFdValueSize(fd);
- int fd_max_entries = bpfGetFdMaxEntries(fd);
- int fd_map_flags = bpfGetFdMapFlags(fd);
-
- // DEVMAPs are readonly from the bpf program side's point of view, as such
- // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
- int desired_map_flags = (int)mapDef.map_flags;
- if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
- desired_map_flags |= BPF_F_RDONLY_PROG;
-
- // The .h file enforces that this is a power of two, and page size will
- // also always be a power of two, so this logic is actually enough to
- // force it to be a multiple of the page size, as required by the kernel.
- unsigned int desired_max_entries = mapDef.max_entries;
- if (type == BPF_MAP_TYPE_RINGBUF) {
- if (desired_max_entries < page_size) desired_max_entries = page_size;
- }
-
- // The following checks should *never* trigger, if one of them somehow does,
- // it probably means a bpf .o file has been changed/replaced at runtime
- // and bpfloader was manually rerun (normally it should only run *once*
- // early during the boot process).
- // Another possibility is that something is misconfigured in the code:
- // most likely a shared map is declared twice differently.
- // But such a change should never be checked into the source tree...
- if ((fd_type == type) &&
- (fd_key_size == (int)mapDef.key_size) &&
- (fd_value_size == (int)mapDef.value_size) &&
- (fd_max_entries == (int)desired_max_entries) &&
- (fd_map_flags == desired_map_flags)) {
- return true;
- }
-
- ALOGE("bpf map name %s mismatch: desired/found: "
- "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
- mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
- fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
- return false;
-}
-
-static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
- const char* prefix, const size_t sizeOfBpfMapDef,
- const unsigned int bpfloader_ver) {
- int ret;
- vector<char> mdData;
- vector<struct bpf_map_def> md;
- vector<string> mapNames;
- string objName = pathToObjName(string(elfPath));
-
- ret = readSectionByName("maps", elfFile, mdData);
- if (ret == -2) return 0; // no maps to read
- if (ret) return ret;
-
- if (mdData.size() % sizeOfBpfMapDef) {
- ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
- mdData.size(), sizeOfBpfMapDef);
- return -1;
- };
-
- int mapCount = mdData.size() / sizeOfBpfMapDef;
- md.resize(mapCount);
- size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
-
- const char* dataPtr = mdData.data();
- for (auto& m : md) {
- // First we zero initialize
- memset(&m, 0, sizeof(m));
- // Then we set non-zero defaults
- m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER; // v1.0
- m.max_kver = 0xFFFFFFFFu; // matches KVER_INF from bpf_helpers.h
- // Then we copy over the structure prefix from the ELF file.
- memcpy(&m, dataPtr, trimmedSize);
- // Move to next struct in the ELF file
- dataPtr += sizeOfBpfMapDef;
- }
-
- ret = getSectionSymNames(elfFile, "maps", mapNames);
- if (ret) return ret;
-
- unsigned kvers = kernelVersion();
-
- for (int i = 0; i < (int)mapNames.size(); i++) {
- if (md[i].zero != 0) abort();
-
- if (bpfloader_ver < md[i].bpfloader_min_ver) {
- ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
- md[i].bpfloader_min_ver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if (bpfloader_ver >= md[i].bpfloader_max_ver) {
- ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
- md[i].bpfloader_max_ver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if (kvers < md[i].min_kver) {
- ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
- mapNames[i].c_str(), kvers, md[i].min_kver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if (kvers >= md[i].max_kver) {
- ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
- mapNames[i].c_str(), kvers, md[i].max_kver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
- (md[i].ignore_on_userdebug && isUserdebug())) {
- ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
- getBuildType().c_str());
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
- (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
- (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
- (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
- (isRiscV() && md[i].ignore_on_riscv64)) {
- ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
- describeArch());
- mapFds.push_back(unique_fd());
- continue;
- }
-
- enum bpf_map_type type = md[i].type;
- if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
- // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
- // of be approximated: ARRAY has the same userspace api, though it is not usable
- // by the same ebpf programs. However, that's okay because the bpf_redirect_map()
- // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
- // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
- // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
- // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
- type = BPF_MAP_TYPE_ARRAY;
- }
- if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
- // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
- // of be approximated: HASH has the same userspace visible api.
- // However it cannot be used by ebpf programs in the same way.
- // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
- // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
- // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
- // programs as being 5.4+...
- type = BPF_MAP_TYPE_HASH;
- }
-
- // The .h file enforces that this is a power of two, and page size will
- // also always be a power of two, so this logic is actually enough to
- // force it to be a multiple of the page size, as required by the kernel.
- unsigned int max_entries = md[i].max_entries;
- if (type == BPF_MAP_TYPE_RINGBUF) {
- if (max_entries < page_size) max_entries = page_size;
- }
-
- domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
- if (specified(selinux_context)) {
- ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
- md[i].selinux_context, static_cast<int>(selinux_context),
- lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
- }
-
- domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
- if (unrecognized(pin_subdir)) return -ENOTDIR;
- if (specified(pin_subdir)) {
- ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
- static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
- }
-
- // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
- // except that maps shared across .o's have empty <objName>
- // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
- string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
- (md[i].shared ? "" : objName) + "_" + mapNames[i];
- bool reuse = false;
- unique_fd fd;
- int saved_errno;
-
- if (access(mapPinLoc.c_str(), F_OK) == 0) {
- fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
- saved_errno = errno;
- ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
- reuse = true;
- } else {
- union bpf_attr req = {
- .map_type = type,
- .key_size = md[i].key_size,
- .value_size = md[i].value_size,
- .max_entries = max_entries,
- .map_flags = md[i].map_flags,
- };
- if (isAtLeastKernelVersion(4, 15, 0))
- strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
- fd.reset(bpf(BPF_MAP_CREATE, req));
- saved_errno = errno;
- ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
- }
-
- if (!fd.ok()) return -saved_errno;
-
- // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
- // safety (since reuse code path is rare) run these checks even if we just created it.
- // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
- if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
-
- if (!reuse) {
- if (specified(selinux_context)) {
- string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
- "tmp_map_" + objName + "_" + mapNames[i];
- ret = bpfFdPin(fd, createLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- ret = renameat2(AT_FDCWD, createLoc.c_str(),
- AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
- if (ret) {
- int err = errno;
- ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
- err, strerror(err));
- return -err;
- }
- } else {
- ret = bpfFdPin(fd, mapPinLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- }
- ret = chmod(mapPinLoc.c_str(), md[i].mode);
- if (ret) {
- int err = errno;
- ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
- strerror(err));
- return -err;
- }
- ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
- if (ret) {
- int err = errno;
- ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
- ret, err, strerror(err));
- return -err;
- }
- }
-
- int mapId = bpfGetFdMapId(fd);
- if (mapId == -1) {
- ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
- } else {
- ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
- }
-
- mapFds.push_back(std::move(fd));
- }
-
- return ret;
-}
-
-/* For debugging, dump all instructions */
-static void dumpIns(char* ins, int size) {
- for (int row = 0; row < size / 8; row++) {
- ALOGE("%d: ", row);
- for (int j = 0; j < 8; j++) {
- ALOGE("%3x ", ins[(row * 8) + j]);
- }
- ALOGE("\n");
- }
-}
-
-/* For debugging, dump all code sections from cs list */
-static void dumpAllCs(vector<codeSection>& cs) {
- for (int i = 0; i < (int)cs.size(); i++) {
- ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
- dumpIns((char*)cs[i].data.data(), cs[i].data.size());
- ALOGE("-----------");
- }
-}
-
-static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
- int insnIndex;
- struct bpf_insn *insn, *insns;
-
- insns = (struct bpf_insn*)(insnsPtr);
-
- insnIndex = offset / sizeof(struct bpf_insn);
- insn = &insns[insnIndex];
-
- // Occasionally might be useful for relocation debugging, but pretty spammy
- if (0) {
- ALOGV("applying relo to instruction at byte offset: %llu, "
- "insn offset %d, insn %llx",
- (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
- }
-
- if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
- ALOGE("Dumping all instructions till ins %d", insnIndex);
- ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
- dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
- return;
- }
-
- insn->imm = fd;
- insn->src_reg = BPF_PSEUDO_MAP_FD;
-}
-
-static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
- vector<string> mapNames;
-
- int ret = getSectionSymNames(elfFile, "maps", mapNames);
- if (ret) return;
-
- for (int k = 0; k != (int)cs.size(); k++) {
- Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
- int n_rel = cs[k].rel_data.size() / sizeof(*rel);
-
- for (int i = 0; i < n_rel; i++) {
- int symIndex = ELF64_R_SYM(rel[i].r_info);
- string symName;
-
- ret = getSymNameByIdx(elfFile, symIndex, symName);
- if (ret) return;
-
- /* Find the map fd and apply relo */
- for (int j = 0; j < (int)mapNames.size(); j++) {
- if (!mapNames[j].compare(symName)) {
- applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
- break;
- }
- }
- }
- }
-}
-
-static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
- const char* prefix, const unsigned int bpfloader_ver) {
- unsigned kvers = kernelVersion();
-
- if (!kvers) {
- ALOGE("unable to get kernel version");
- return -EINVAL;
- }
-
- string objName = pathToObjName(string(elfPath));
-
- for (int i = 0; i < (int)cs.size(); i++) {
- unique_fd& fd = cs[i].prog_fd;
- int ret;
- string name = cs[i].name;
-
- if (!cs[i].prog_def.has_value()) {
- ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
- return -EINVAL;
- }
-
- unsigned min_kver = cs[i].prog_def->min_kver;
- unsigned max_kver = cs[i].prog_def->max_kver;
- ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
- max_kver, kvers);
- if (kvers < min_kver) continue;
- if (kvers >= max_kver) continue;
-
- unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
- unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
- domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
- domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
- // Note: make sure to only check for unrecognized *after* verifying bpfloader
- // version limits include this bpfloader's version.
-
- ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
- bpfMinVer, bpfMaxVer);
- if (bpfloader_ver < bpfMinVer) continue;
- if (bpfloader_ver >= bpfMaxVer) continue;
-
- if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
- (cs[i].prog_def->ignore_on_user && isUser()) ||
- (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
- ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
- getBuildType().c_str());
- continue;
- }
-
- if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
- (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
- (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
- (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
- (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
- ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
- continue;
- }
-
- if (unrecognized(pin_subdir)) return -ENOTDIR;
-
- if (specified(selinux_context)) {
- ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
- cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
- lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
- }
-
- if (specified(pin_subdir)) {
- ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
- cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
- lookupPinSubdir(pin_subdir));
- }
-
- // strip any potential $foo suffix
- // this can be used to provide duplicate programs
- // conditionally loaded based on running kernel version
- name = name.substr(0, name.find_last_of('$'));
-
- bool reuse = false;
- // Format of pin location is
- // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
- string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
- objName + '_' + string(name);
- if (access(progPinLoc.c_str(), F_OK) == 0) {
- fd.reset(retrieveProgram(progPinLoc.c_str()));
- ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
- (!fd.ok() ? std::strerror(errno) : "no error"));
- reuse = true;
- } else {
- vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
-
- union bpf_attr req = {
- .prog_type = cs[i].type,
- .kern_version = kvers,
- .license = ptr_to_u64(license.c_str()),
- .insns = ptr_to_u64(cs[i].data.data()),
- .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
- .log_level = 1,
- .log_buf = ptr_to_u64(log_buf.data()),
- .log_size = static_cast<__u32>(log_buf.size()),
- .expected_attach_type = cs[i].expected_attach_type,
- };
- if (isAtLeastKernelVersion(4, 15, 0))
- strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
- fd.reset(bpf(BPF_PROG_LOAD, req));
-
- ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
- cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
-
- if (!fd.ok()) {
- vector<string> lines = android::base::Split(log_buf.data(), "\n");
-
- ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
- for (const auto& line : lines) ALOGW("%s", line.c_str());
- ALOGW("BPF_PROG_LOAD - END log_buf contents.");
-
- if (cs[i].prog_def->optional) {
- ALOGW("failed program is marked optional - continuing...");
- continue;
- }
- ALOGE("non-optional program failed to load.");
- }
- }
-
- if (!fd.ok()) return fd.get();
-
- if (!reuse) {
- if (specified(selinux_context)) {
- string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
- "tmp_prog_" + objName + '_' + string(name);
- ret = bpfFdPin(fd, createLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- ret = renameat2(AT_FDCWD, createLoc.c_str(),
- AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
- if (ret) {
- int err = errno;
- ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
- err, strerror(err));
- return -err;
- }
- } else {
- ret = bpfFdPin(fd, progPinLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- }
- if (chmod(progPinLoc.c_str(), 0440)) {
- int err = errno;
- ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
- return -err;
- }
- if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
- (gid_t)cs[i].prog_def->gid)) {
- int err = errno;
- ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
- cs[i].prog_def->gid, err, strerror(err));
- return -err;
- }
- }
-
- int progId = bpfGetFdProgId(fd);
- if (progId == -1) {
- ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
- } else {
- ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
- }
- }
-
- return 0;
-}
-
-int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
- const Location& location) {
- vector<char> license;
- vector<char> critical;
- vector<codeSection> cs;
- vector<unique_fd> mapFds;
- int ret;
-
- if (!isCritical) return -1;
- *isCritical = false;
-
- ifstream elfFile(elfPath, ios::in | ios::binary);
- if (!elfFile.is_open()) return -1;
-
- ret = readSectionByName("critical", elfFile, critical);
- *isCritical = !ret;
-
- ret = readSectionByName("license", elfFile, license);
- if (ret) {
- ALOGE("Couldn't find license in %s", elfPath);
- return ret;
- } else {
- ALOGD("Loading %s%s ELF object %s with license %s",
- *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
- elfPath, (char*)license.data());
- }
-
- // the following default values are for bpfloader V0.0 format which does not include them
- unsigned int bpfLoaderMinVer =
- readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
- unsigned int bpfLoaderMaxVer =
- readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
- unsigned int bpfLoaderMinRequiredVer =
- readSectionUint("bpfloader_min_required_ver", elfFile, 0);
- unsigned int netBpfLoadMinVer =
- readSectionUint("netbpfload_min_ver", elfFile, 0);
- size_t sizeOfBpfMapDef =
- readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
- size_t sizeOfBpfProgDef =
- readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
-
- // temporary hack to enable gentle enablement of mainline NetBpfLoad
- if (bpfloader_ver < netBpfLoadMinVer) {
- ALOGI("NetBpfLoad version %d ignoring ELF object %s with netbpfload min ver %d",
- bpfloader_ver, elfPath, netBpfLoadMinVer);
- return 0;
- }
-
- // inclusive lower bound check
- if (bpfloader_ver < bpfLoaderMinVer) {
- ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
- bpfloader_ver, elfPath, bpfLoaderMinVer);
- return 0;
- }
-
- // exclusive upper bound check
- if (bpfloader_ver >= bpfLoaderMaxVer) {
- ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
- bpfloader_ver, elfPath, bpfLoaderMaxVer);
- return 0;
- }
-
- if (bpfloader_ver < bpfLoaderMinRequiredVer) {
- ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
- bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
- return -1;
- }
-
- ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
- bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
-
- if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
- ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
- DEFAULT_SIZEOF_BPF_MAP_DEF);
- return -1;
- }
-
- if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
- ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
- DEFAULT_SIZEOF_BPF_PROG_DEF);
- return -1;
- }
-
- ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
- if (ret) {
- ALOGE("Couldn't read all code sections in %s", elfPath);
- return ret;
- }
-
- /* Just for future debugging */
- if (0) dumpAllCs(cs);
-
- ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
- if (ret) {
- ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
- return ret;
- }
-
- for (int i = 0; i < (int)mapFds.size(); i++)
- ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
-
- applyMapRelo(elfFile, mapFds, cs);
-
- ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
- if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
-
- return ret;
-}
-
-} // namespace bpf
-} // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
deleted file mode 100644
index 4da6830..0000000
--- a/netbpfload/loader.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2018-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.
- */
-
-#pragma once
-
-#include <linux/bpf.h>
-
-#include <fstream>
-
-namespace android {
-namespace bpf {
-
-// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
-//
-// The BpfLoader needs to convert these bpf.o specified strings into an enum
-// for internal use (to check that valid values were specified for the specific
-// location of the bpf.o file).
-//
-// It also needs to map selinux_context's into pin_subdir's.
-// This is because of how selinux_context is actually implemented via pin+rename.
-//
-// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
-// is aware of. Thus there currently needs to be a 1:1 mapping between the two.
-//
-enum class domain : int {
- unrecognized = -1, // invalid for this version of the bpfloader
- unspecified = 0, // means just use the default for that specific pin location
- tethering, // (S+) fs_bpf_tethering /sys/fs/bpf/tethering
- net_private, // (T+) fs_bpf_net_private /sys/fs/bpf/net_private
- net_shared, // (T+) fs_bpf_net_shared /sys/fs/bpf/net_shared
- netd_readonly, // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
- netd_shared, // (T+) fs_bpf_netd_shared /sys/fs/bpf/netd_shared
-};
-
-// Note: this does not include domain::unrecognized, but does include domain::unspecified
-static constexpr domain AllDomains[] = {
- domain::unspecified,
- domain::tethering,
- domain::net_private,
- domain::net_shared,
- domain::netd_readonly,
- domain::netd_shared,
-};
-
-static constexpr bool unrecognized(domain d) {
- return d == domain::unrecognized;
-}
-
-// Note: this doesn't handle unrecognized, handle it first.
-static constexpr bool specified(domain d) {
- return d != domain::unspecified;
-}
-
-struct Location {
- const char* const dir = "";
- const char* const prefix = "";
-};
-
-// BPF loader implementation. Loads an eBPF ELF object
-int loadProg(const char* elfPath, bool* isCritical, const unsigned int bpfloader_ver,
- const Location &location = {});
-
-// Exposed for testing
-unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
-
-// Returns the build type string (from ro.build.type).
-const std::string& getBuildType();
-
-// The following functions classify the 3 Android build types.
-inline bool isEng() {
- return getBuildType() == "eng";
-}
-inline bool isUser() {
- return getBuildType() == "user";
-}
-inline bool isUserdebug() {
- return getBuildType() == "userdebug";
-}
-
-} // namespace bpf
-} // namespace android
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
new file mode 100644
index 0000000..1a4130a
--- /dev/null
+++ b/networksecurity/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1479456
+
+sandrom@google.com
+tweek@google.com
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
new file mode 100644
index 0000000..20ecbce
--- /dev/null
+++ b/networksecurity/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "NetworkSecurityUnitTests"
+ }
+ ]
+}
diff --git a/tests/cts/hostside-network-policy/aidl/Android.bp b/networksecurity/framework/Android.bp
similarity index 72%
rename from tests/cts/hostside-network-policy/aidl/Android.bp
rename to networksecurity/framework/Android.bp
index b182090..2b77926 100644
--- a/tests/cts/hostside-network-policy/aidl/Android.bp
+++ b/networksecurity/framework/Android.bp
@@ -1,3 +1,4 @@
+//
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -11,16 +12,20 @@
// 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_framework_backstage_power",
+ default_team: "trendy_team_platform_security",
default_applicable_licenses: ["Android-Apache-2.0"],
}
-java_test_helper_library {
- name: "CtsHostsideNetworkPolicyTestsAidl",
- sdk_version: "current",
+filegroup {
+ name: "framework-networksecurity-sources",
srcs: [
- "com/android/cts/netpolicy/hostside/*.aidl",
+ "src/**/*.java",
+ "src/**/*.aidl",
+ ],
+ path: "src",
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
],
}
diff --git a/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
new file mode 100644
index 0000000..94521ae
--- /dev/null
+++ b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ct;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemService;
+
+import com.android.net.ct.flags.Flags;
+
+/**
+ * Provides the primary API for the Certificate Transparency Manager.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CERTIFICATE_TRANSPARENCY_SERVICE)
+@SystemService(CertificateTransparencyManager.SERVICE_NAME)
+public final class CertificateTransparencyManager {
+
+ public static final String SERVICE_NAME = "certificate_transparency";
+
+ /**
+ * Creates a new CertificateTransparencyManager instance.
+ *
+ * @hide
+ */
+ public CertificateTransparencyManager() {}
+}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
similarity index 61%
copy from tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
copy to networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
index 7aac2ab..b5bce7f 100644
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
+++ b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
@@ -1,11 +1,11 @@
-/*
- * Copyright (C) 2024 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -13,14 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package android.net.ct;
-package com.android.cts.netpolicy.hostside;
-
-import android.net.NetworkInfo;
-
-@JavaDerive(toString=true)
-parcelable NetworkCheckResult {
- boolean connected;
- String details;
- NetworkInfo networkInfo;
-}
\ No newline at end of file
+/**
+* Interface for communicating with CertificateTransparencyService.
+* @hide
+*/
+interface ICertificateTransparencyManager {}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
new file mode 100644
index 0000000..52667ae
--- /dev/null
+++ b/networksecurity/service/Android.bp
@@ -0,0 +1,41 @@
+// 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_team: "trendy_team_platform_security",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Main lib for Certificate Transparency services.
+java_library {
+ name: "service-networksecurity-pre-jarjar",
+ defaults: ["framework-system-server-module-defaults"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ libs: [
+ "framework-configinfrastructure.stubs.module_lib",
+ "framework-connectivity-pre-jarjar",
+ "service-connectivity-pre-jarjar",
+ ],
+
+ // This is included in service-connectivity which is 30+
+ // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
+ // (service-connectivity is only used on 31+) and use 31 here
+ min_sdk_version: "30",
+ sdk_version: "system_server_current",
+ apex_available: ["com.android.tethering"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
new file mode 100644
index 0000000..b2ef345
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -0,0 +1,243 @@
+/*
+ * 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.ct;
+
+import android.annotation.RequiresApi;
+import android.app.DownloadManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.Signature;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+/** Helper class to download certificate transparency log files. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CertificateTransparencyDownloader extends BroadcastReceiver {
+
+ private static final String TAG = "CertificateTransparencyDownloader";
+
+ // TODO: move key to a DeviceConfig flag.
+ private static final byte[] PUBLIC_KEY_BYTES =
+ Base64.getDecoder()
+ .decode(
+ "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsu0BHGnQ++W2CTdyZyxv"
+ + "HHRALOZPlnu/VMVgo2m+JZ8MNbAOH2cgXb8mvOj8flsX/qPMuKIaauO+PwROMjiq"
+ + "fUpcFm80Kl7i97ZQyBDYKm3MkEYYpGN+skAR2OebX9G2DfDqFY8+jUpOOWtBNr3L"
+ + "rmVcwx+FcFdMjGDlrZ5JRmoJ/SeGKiORkbbu9eY1Wd0uVhz/xI5bQb0OgII7hEj+"
+ + "i/IPbJqOHgB8xQ5zWAJJ0DmG+FM6o7gk403v6W3S8qRYiR84c50KppGwe4YqSMkF"
+ + "bLDleGQWLoaDSpEWtESisb4JiLaY4H+Kk0EyAhPSb+49JfUozYl+lf7iFN3qRq/S"
+ + "IXXTh6z0S7Qa8EYDhKGCrpI03/+qprwy+my6fpWHi6aUIk4holUCmWvFxZDfixox"
+ + "K0RlqbFDl2JXMBquwlQpm8u5wrsic1ksIv9z8x9zh4PJqNpCah0ciemI3YGRQqSe"
+ + "/mRRXBiSn9YQBUPcaeqCYan+snGADFwHuXCd9xIAdFBolw9R9HTedHGUfVXPJDiF"
+ + "4VusfX6BRR/qaadB+bqEArF/TzuDUr6FvOR4o8lUUxgLuZ/7HO+bHnaPFKYHHSm+"
+ + "+z1lVDhhYuSZ8ax3T0C3FZpb7HMjZtpEorSV5ElKJEJwrhrBCMOD8L01EoSPrGlS"
+ + "1w22i9uGHMn/uGQKo28u7AsCAwEAAQ==");
+
+ private final Context mContext;
+ private final DataStore mDataStore;
+ private final DownloadHelper mDownloadHelper;
+ private final CertificateTransparencyInstaller mInstaller;
+ private final byte[] mPublicKey;
+
+ @VisibleForTesting
+ CertificateTransparencyDownloader(
+ Context context,
+ DataStore dataStore,
+ DownloadHelper downloadHelper,
+ CertificateTransparencyInstaller installer,
+ byte[] publicKey) {
+ mContext = context;
+ mDataStore = dataStore;
+ mDownloadHelper = downloadHelper;
+ mInstaller = installer;
+ mPublicKey = publicKey;
+ }
+
+ CertificateTransparencyDownloader(Context context, DataStore dataStore) {
+ this(
+ context,
+ dataStore,
+ new DownloadHelper(context),
+ new CertificateTransparencyInstaller(),
+ PUBLIC_KEY_BYTES);
+ }
+
+ void registerReceiver() {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ }
+ }
+
+ void startMetadataDownload(String metadataUrl) {
+ long downloadId = download(metadataUrl);
+ if (downloadId == -1) {
+ Log.e(TAG, "Metadata download request failed for " + metadataUrl);
+ return;
+ }
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
+ mDataStore.store();
+ }
+
+ void startContentDownload(String contentUrl) {
+ long downloadId = download(contentUrl);
+ if (downloadId == -1) {
+ Log.e(TAG, "Content download request failed for " + contentUrl);
+ return;
+ }
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
+ mDataStore.store();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
+ Log.w(TAG, "Received unexpected broadcast with action " + action);
+ return;
+ }
+
+ long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ if (completedId == -1) {
+ Log.e(TAG, "Invalid completed download Id");
+ return;
+ }
+
+ if (isMetadataDownloadId(completedId)) {
+ handleMetadataDownloadCompleted(completedId);
+ return;
+ }
+
+ if (isContentDownloadId(completedId)) {
+ handleContentDownloadCompleted(completedId);
+ return;
+ }
+
+ Log.e(TAG, "Download id " + completedId + " is neither metadata nor content.");
+ }
+
+ private void handleMetadataDownloadCompleted(long downloadId) {
+ if (!mDownloadHelper.isSuccessful(downloadId)) {
+ Log.w(TAG, "Metadata download failed.");
+ // TODO: re-attempt download
+ return;
+ }
+
+ startContentDownload(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
+ }
+
+ private void handleContentDownloadCompleted(long downloadId) {
+ if (!mDownloadHelper.isSuccessful(downloadId)) {
+ Log.w(TAG, "Content download failed.");
+ // TODO: re-attempt download
+ return;
+ }
+
+ Uri contentUri = getContentDownloadUri();
+ Uri metadataUri = getMetadataDownloadUri();
+ if (contentUri == null || metadataUri == null) {
+ Log.e(TAG, "Invalid URIs");
+ return;
+ }
+
+ boolean success = false;
+ try {
+ success = verify(contentUri, metadataUri);
+ } catch (IOException | GeneralSecurityException e) {
+ Log.e(TAG, "Could not verify new log list", e);
+ }
+ if (!success) {
+ Log.w(TAG, "Log list did not pass verification");
+ return;
+ }
+
+ // TODO: validate file content.
+
+ String version = mDataStore.getProperty(Config.VERSION_PENDING);
+ String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
+ String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
+ try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
+ success = mInstaller.install(inputStream, version);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not install new content", e);
+ return;
+ }
+
+ if (success) {
+ // Update information about the stored version on successful install.
+ mDataStore.setProperty(Config.VERSION, version);
+ mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+ mDataStore.setProperty(Config.METADATA_URL, metadataUrl);
+ mDataStore.store();
+ }
+ }
+
+ private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ verifier.initVerify(keyFactory.generatePublic(new X509EncodedKeySpec(mPublicKey)));
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ try (InputStream fileStream = contentResolver.openInputStream(file);
+ InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ verifier.update(fileStream.readAllBytes());
+ return verifier.verify(signatureStream.readAllBytes());
+ }
+ }
+
+ private long download(String url) {
+ try {
+ return mDownloadHelper.startDownload(url);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Download request failed", e);
+ return -1;
+ }
+ }
+
+ @VisibleForTesting
+ boolean isMetadataDownloadId(long downloadId) {
+ return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+ }
+
+ @VisibleForTesting
+ boolean isContentDownloadId(long downloadId) {
+ return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+ }
+
+ private Uri getMetadataDownloadUri() {
+ return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+ }
+
+ private Uri getContentDownloadUri() {
+ return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
new file mode 100644
index 0000000..a263546
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ct;
+
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.os.Build;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.concurrent.Executors;
+
+/** Listener class for the Certificate Transparency Phenotype flags. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
+
+ private static final String TAG = "CertificateTransparencyFlagsListener";
+
+ private final DataStore mDataStore;
+ private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+ CertificateTransparencyFlagsListener(Context context) {
+ mDataStore = new DataStore(Config.PREFERENCES_FILE);
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(context, mDataStore);
+ }
+
+ void initialize() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.registerReceiver();
+ DeviceConfig.addOnPropertiesChangedListener(
+ Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
+ }
+ // TODO: handle property changes triggering on boot before registering this listener.
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+ return;
+ }
+
+ String newVersion =
+ DeviceConfig.getString(Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_VERSION, "");
+ String newContentUrl =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_CONTENT_URL, "");
+ String newMetadataUrl =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_METADATA_URL, "");
+ if (TextUtils.isEmpty(newVersion)
+ || TextUtils.isEmpty(newContentUrl)
+ || TextUtils.isEmpty(newMetadataUrl)) {
+ return;
+ }
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "newVersion=" + newVersion);
+ Log.d(TAG, "newContentUrl=" + newContentUrl);
+ Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
+ }
+
+ String oldVersion = mDataStore.getProperty(Config.VERSION);
+ String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
+ String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
+
+ if (TextUtils.equals(newVersion, oldVersion)
+ && TextUtils.equals(newContentUrl, oldContentUrl)
+ && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
+ Log.i(TAG, "No flag changed, ignoring update");
+ return;
+ }
+
+ // TODO: handle the case where there is already a pending download.
+
+ mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
+ mDataStore.store();
+
+ mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
new file mode 100644
index 0000000..82dcadf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -0,0 +1,162 @@
+/*
+ * 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.ct;
+
+import android.annotation.SuppressLint;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/** Installer of CT log lists. */
+public class CertificateTransparencyInstaller {
+
+ private static final String TAG = "CertificateTransparencyInstaller";
+ private static final String CT_DIR_NAME = "/data/misc/keychain/ct/";
+
+ static final String LOGS_DIR_PREFIX = "logs-";
+ static final String LOGS_LIST_FILE_NAME = "log_list.json";
+ static final String CURRENT_DIR_SYMLINK_NAME = "current";
+
+ private final File mCertificateTransparencyDir;
+ private final File mCurrentDirSymlink;
+
+ CertificateTransparencyInstaller(File certificateTransparencyDir) {
+ mCertificateTransparencyDir = certificateTransparencyDir;
+ mCurrentDirSymlink = new File(certificateTransparencyDir, CURRENT_DIR_SYMLINK_NAME);
+ }
+
+ CertificateTransparencyInstaller() {
+ this(new File(CT_DIR_NAME));
+ }
+
+ /**
+ * Install a new log list to use during SCT verification.
+ *
+ * @param newContent an input stream providing the log list
+ * @param version the version of the new log list
+ * @return true if the log list was installed successfully, false otherwise.
+ * @throws IOException if the list cannot be saved in the CT directory.
+ */
+ public boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new there's a
+ // bunch of steps. We create a new directory with the logs and then do an atomic update of
+ // the current symlink to point to the new directory.
+ // 1. Ensure that the update dir exists and is readable.
+ makeDir(mCertificateTransparencyDir);
+
+ File newLogsDir = new File(mCertificateTransparencyDir, LOGS_DIR_PREFIX + version);
+ // 2. Handle the corner case where the new directory already exists.
+ if (newLogsDir.exists()) {
+ // If the symlink has already been updated then the update died between steps 6 and 7
+ // and so we cannot delete the directory since it is in use.
+ if (newLogsDir.getCanonicalPath().equals(mCurrentDirSymlink.getCanonicalPath())) {
+ deleteOldLogDirectories();
+ return false;
+ }
+ // If the symlink has not been updated then the previous installation failed and this is
+ // a re-attempt. Clean-up leftover files and try again.
+ deleteContentsAndDir(newLogsDir);
+ }
+ try {
+ // 3. Create /data/misc/keychain/ct/logs-<new_version>/ .
+ makeDir(newLogsDir);
+
+ // 4. Move the log list json file in logs-<new_version>/ .
+ File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
+ if (Files.copy(newContent, logListFile.toPath()) == 0) {
+ throw new IOException("The log list appears empty");
+ }
+ setWorldReadable(logListFile);
+
+ // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
+ File tempSymlink = new File(mCertificateTransparencyDir, "new_symlink");
+ try {
+ Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to create symlink", e);
+ }
+
+ // 6. Update the symlink target, this is the actual update step.
+ tempSymlink.renameTo(mCurrentDirSymlink.getAbsoluteFile());
+ } catch (IOException | RuntimeException e) {
+ deleteContentsAndDir(newLogsDir);
+ throw e;
+ }
+ Log.i(TAG, "CT log directory updated to " + newLogsDir.getAbsolutePath());
+ // 7. Cleanup
+ deleteOldLogDirectories();
+ return true;
+ }
+
+ private void makeDir(File dir) throws IOException {
+ dir.mkdir();
+ if (!dir.isDirectory()) {
+ throw new IOException("Unable to make directory " + dir.getCanonicalPath());
+ }
+ setWorldReadable(dir);
+ }
+
+ // CT files and directories are readable by all apps.
+ @SuppressLint("SetWorldReadable")
+ private void setWorldReadable(File file) throws IOException {
+ if (!file.setReadable(true, false)) {
+ throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
+ }
+ }
+
+ private void deleteOldLogDirectories() throws IOException {
+ if (!mCertificateTransparencyDir.exists()) {
+ return;
+ }
+ File currentTarget = mCurrentDirSymlink.getCanonicalFile();
+ for (File file : mCertificateTransparencyDir.listFiles()) {
+ if (!currentTarget.equals(file.getCanonicalFile())
+ && file.getName().startsWith(LOGS_DIR_PREFIX)) {
+ deleteContentsAndDir(file);
+ }
+ }
+ }
+
+ static boolean deleteContentsAndDir(File dir) {
+ if (deleteContents(dir)) {
+ return dir.delete();
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean deleteContents(File dir) {
+ File[] files = dir.listFiles();
+ boolean success = true;
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ success &= deleteContents(file);
+ }
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete " + file);
+ success = false;
+ }
+ }
+ }
+ return success;
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
new file mode 100644
index 0000000..edf7c56
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -0,0 +1,61 @@
+/*
+ * 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.ct;
+
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.net.ct.ICertificateTransparencyManager;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import com.android.net.ct.flags.Flags;
+import com.android.server.SystemService;
+
+/** Implementation of the Certificate Transparency service. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+
+ private final CertificateTransparencyFlagsListener mFlagsListener;
+
+ /**
+ * @return true if the CertificateTransparency service is enabled.
+ */
+ public static boolean enabled(Context context) {
+ return DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_SERVICE_ENABLED, false)
+ && Flags.certificateTransparencyService();
+ }
+
+ /** Creates a new {@link CertificateTransparencyService} object. */
+ public CertificateTransparencyService(Context context) {
+ mFlagsListener = new CertificateTransparencyFlagsListener(context);
+ }
+
+ /**
+ * Called by {@link com.android.server.ConnectivityServiceInitializer}.
+ *
+ * @see com.android.server.SystemService#onBootPhase
+ */
+ public void onBootPhase(int phase) {
+
+ switch (phase) {
+ case SystemService.PHASE_BOOT_COMPLETED:
+ mFlagsListener.initialize();
+ break;
+ default:
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
new file mode 100644
index 0000000..2a6b8e2
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ct;
+
+import android.content.ApexEnvironment;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+import java.io.File;
+
+/** Class holding the constants used by the CT feature. */
+final class Config {
+
+ static final boolean DEBUG = false;
+
+ // preferences file
+ private static final File DEVICE_PROTECTED_DATA_DIR =
+ ApexEnvironment.getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ private static final String PREFERENCES_FILE_NAME = "ct.preferences";
+ static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
+
+ // Phenotype flags
+ static final String NAMESPACE_NETWORK_SECURITY = "network_security";
+ private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
+ static final String FLAG_SERVICE_ENABLED = FLAGS_PREFIX + "service_enabled";
+ static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
+ static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
+ static final String FLAG_VERSION = FLAGS_PREFIX + "version";
+
+ // properties
+ static final String VERSION_PENDING = "version_pending";
+ static final String VERSION = "version";
+ static final String CONTENT_URL_PENDING = "content_url_pending";
+ static final String CONTENT_URL = "content_url";
+ static final String CONTENT_URL_KEY = "content_url_key";
+ static final String METADATA_URL_PENDING = "metadata_url_pending";
+ static final String METADATA_URL = "metadata_url";
+ static final String METADATA_URL_KEY = "metadata_url_key";
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
new file mode 100644
index 0000000..cd6aebf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -0,0 +1,67 @@
+/*
+ * 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.ct;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.Properties;
+
+/** Class to persist data needed by CT. */
+class DataStore extends Properties {
+
+ private static final String TAG = "CertificateTransparency";
+
+ private final File mPropertyFile;
+
+ DataStore(File file) {
+ super();
+ mPropertyFile = file;
+ }
+
+ void load() {
+ if (!mPropertyFile.exists()) {
+ return;
+ }
+ try (InputStream in = new FileInputStream(mPropertyFile)) {
+ load(in);
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading property store", e);
+ }
+ }
+
+ void store() {
+ try (OutputStream out = new FileOutputStream(mPropertyFile)) {
+ store(out, "");
+ } catch (IOException e) {
+ Log.e(TAG, "Error storing property store", e);
+ }
+ }
+
+ long getPropertyLong(String key, long defaultValue) {
+ return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
+ }
+
+ Object setPropertyLong(String key, long value) {
+ return setProperty(key, Long.toString(value));
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
new file mode 100644
index 0000000..cc8c4c0
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
@@ -0,0 +1,90 @@
+/*
+ * 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.ct;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Class to handle downloads for Certificate Transparency. */
+public class DownloadHelper {
+
+ private final DownloadManager mDownloadManager;
+
+ @VisibleForTesting
+ DownloadHelper(DownloadManager downloadManager) {
+ mDownloadManager = downloadManager;
+ }
+
+ DownloadHelper(Context context) {
+ this(context.getSystemService(DownloadManager.class));
+ }
+
+ /**
+ * Sends a request to start the download of a provided url.
+ *
+ * @param url the url to download
+ * @return a downloadId if the request was created successfully, -1 otherwise.
+ */
+ public long startDownload(String url) {
+ return mDownloadManager.enqueue(
+ new Request(Uri.parse(url))
+ .setAllowedOverRoaming(false)
+ .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
+ .setRequiresCharging(true));
+ }
+
+ /**
+ * Returns true if the specified download completed successfully.
+ *
+ * @param downloadId the download.
+ * @return true if the download completed successfully.
+ */
+ public boolean isSuccessful(long downloadId) {
+ try (Cursor cursor = mDownloadManager.query(new Query().setFilterById(downloadId))) {
+ if (cursor == null) {
+ return false;
+ }
+ if (cursor.moveToFirst()) {
+ int status =
+ cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
+ if (DownloadManager.STATUS_SUCCESSFUL == status) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the URI of the specified download, or null if the download did not complete
+ * successfully.
+ *
+ * @param downloadId the download.
+ * @return the {@link Uri} if the download completed successfully, null otherwise.
+ */
+ public Uri getUri(long downloadId) {
+ if (downloadId == -1) {
+ return null;
+ }
+ return mDownloadManager.getUriForDownloadedFile(downloadId);
+ }
+}
diff --git a/networksecurity/tests/unit/Android.bp b/networksecurity/tests/unit/Android.bp
new file mode 100644
index 0000000..11263cf
--- /dev/null
+++ b/networksecurity/tests/unit/Android.bp
@@ -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 {
+ default_team: "trendy_team_platform_security",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "NetworkSecurityUnitTests",
+ defaults: ["mts-target-sdk-version-current"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+
+ srcs: ["src/**/*.java"],
+
+ libs: [
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "mockito-target-minus-junit4",
+ "service-networksecurity-pre-jarjar",
+ "truth",
+ ],
+
+ sdk_version: "test_current",
+}
diff --git a/networksecurity/tests/unit/AndroidManifest.xml b/networksecurity/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a3f4b7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.android.net.ct">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.net.ct"
+ android:label="NetworkSecurity Mainline Module Tests" />
+</manifest>
diff --git a/networksecurity/tests/unit/AndroidTest.xml b/networksecurity/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..3c94df7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs NetworkSecurity Mainline unit Tests.">
+ <option name="test-tag" value="NetworkSecurityUnitTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NetworkSecurityUnitTests.apk" />
+ </target_preparer>
+
+ <option name="config-descriptor:metadata" key="mainline-param"
+ value="com.google.android.tethering.next.apex" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.net.ct" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+
+ <!-- Only run in MTS 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>
+</configuration>
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
new file mode 100644
index 0000000..a056c35
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+
+/** Tests for the {@link CertificateTransparencyDownloader}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyDownloaderTest {
+
+ @Mock private DownloadHelper mDownloadHelper;
+ @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+
+ private PrivateKey mPrivateKey;
+ private Context mContext;
+ private File mTempFile;
+ private DataStore mDataStore;
+ private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+ @Before
+ public void setUp() throws IOException, NoSuchAlgorithmException {
+ MockitoAnnotations.initMocks(this);
+
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ KeyPair keyPair = instance.generateKeyPair();
+ mPrivateKey = keyPair.getPrivate();
+
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ mDataStore.load();
+
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(
+ mContext,
+ mDataStore,
+ mDownloadHelper,
+ mCertificateTransparencyInstaller,
+ keyPair.getPublic().getEncoded());
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDownloader_startMetadataDownload() {
+ String metadataUrl = "http://test-metadata.org";
+ long downloadId = 666;
+ when(mDownloadHelper.startDownload(metadataUrl)).thenReturn(downloadId);
+
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
+ mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_startContentDownload() {
+ String contentUrl = "http://test-content.org";
+ long downloadId = 666;
+ when(mDownloadHelper.startDownload(contentUrl)).thenReturn(downloadId);
+
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
+ mCertificateTransparencyDownloader.startContentDownload(contentUrl);
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_handleMetadataCompleteSuccessful() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(true);
+
+ long contentId = 666;
+ String contentUrl = "http://test-content.org";
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
+ when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_handleMetadataCompleteFailed() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(false);
+
+ String contentUrl = "http://test-content.org";
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ verify(mDownloadHelper, never()).startDownload(contentUrl);
+ }
+
+ @Test
+ public void testDownloader_handleContentCompleteInstallSuccessful() throws Exception {
+ String version = "666";
+ long contentId = 666;
+ File logListFile = File.createTempFile("log_list", "json");
+ Uri contentUri = Uri.fromFile(logListFile);
+ long metadataId = 123;
+ File metadataFile = sign(logListFile);
+ Uri metadataUri = Uri.fromFile(metadataFile);
+
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
+ when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
+
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mCertificateTransparencyInstaller, times(1)).install(any(), eq(version));
+ assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
+ }
+
+ @Test
+ public void testDownloader_handleContentCompleteInstallFails() throws Exception {
+ String version = "666";
+ long contentId = 666;
+ File logListFile = File.createTempFile("log_list", "json");
+ Uri contentUri = Uri.fromFile(logListFile);
+ long metadataId = 123;
+ File metadataFile = sign(logListFile);
+ Uri metadataUri = Uri.fromFile(metadataFile);
+
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
+ when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ }
+
+ @Test
+ public void testDownloader_handleContentCompleteVerificationFails() throws IOException {
+ String version = "666";
+ long contentId = 666;
+ Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+ long metadataId = 123;
+ Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-wrong_metadata", "sig"));
+
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId) {
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+
+ private void setUpDownloadComplete(
+ String version, long metadataId, Uri metadataUri, long contentId, Uri contentUri)
+ throws IOException {
+ mDataStore.setProperty(Config.VERSION_PENDING, version);
+
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+ when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+ when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+ when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ }
+
+ private File sign(File file) throws IOException, GeneralSecurityException {
+ File signatureFile = File.createTempFile("log_list-metadata", "sig");
+ Signature signer = Signature.getInstance("SHA256withRSA");
+ signer.initSign(mPrivateKey);
+
+ try (InputStream fileStream = new FileInputStream(file);
+ OutputStream outputStream = new FileOutputStream(signatureFile)) {
+ signer.update(fileStream.readAllBytes());
+ outputStream.write(signer.sign());
+ }
+
+ return signatureFile;
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
new file mode 100644
index 0000000..bfb8bdf
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Tests for the {@link CertificateTransparencyInstaller}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyInstallerTest {
+
+ private File mTestDir =
+ new File(
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
+ "test-dir");
+ private File mTestSymlink =
+ new File(mTestDir, CertificateTransparencyInstaller.CURRENT_DIR_SYMLINK_NAME);
+ private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
+ new CertificateTransparencyInstaller(mTestDir);
+
+ @Before
+ public void setUp() {
+ CertificateTransparencyInstaller.deleteContentsAndDir(mTestDir);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
+ String content = "i_am_a_certificate_and_i_am_transparent";
+ String version = "666";
+ boolean success = false;
+
+ try (InputStream inputStream = asStream(content)) {
+ success = mCertificateTransparencyInstaller.install(inputStream, version);
+ }
+
+ assertThat(success).isTrue();
+ assertThat(mTestDir.exists()).isTrue();
+ assertThat(mTestDir.isDirectory()).isTrue();
+ assertThat(mTestSymlink.exists()).isTrue();
+ assertThat(mTestSymlink.isDirectory()).isTrue();
+
+ File logsDir =
+ new File(mTestDir, CertificateTransparencyInstaller.LOGS_DIR_PREFIX + version);
+ assertThat(logsDir.exists()).isTrue();
+ assertThat(logsDir.isDirectory()).isTrue();
+ assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
+
+ File logsListFile = new File(logsDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.exists()).isTrue();
+ assertThat(readAsString(logsListFile)).isEqualTo(content);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
+ throws IOException, ErrnoException {
+ String existingVersion = "666";
+ String existingContent = "i_was_already_installed_successfully";
+ File existingLogDir =
+ new File(
+ mTestDir,
+ CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(mTestDir.mkdir()).isTrue();
+ assertThat(existingLogDir.mkdir()).isTrue();
+ Os.symlink(existingLogDir.getCanonicalPath(), mTestSymlink.getCanonicalPath());
+ File logsListFile =
+ new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ logsListFile.createNewFile();
+ writeToFile(logsListFile, existingContent);
+ boolean success = false;
+
+ try (InputStream inputStream = asStream("i_will_be_ignored")) {
+ success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+ }
+
+ assertThat(success).isFalse();
+ assertThat(readAsString(logsListFile)).isEqualTo(existingContent);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_versionInstalledFailed()
+ throws IOException, ErrnoException {
+ String existingVersion = "666";
+ String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
+ String newContent = "i_am_the_real_certificate";
+ File existingLogDir =
+ new File(
+ mTestDir,
+ CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(mTestDir.mkdir()).isTrue();
+ assertThat(existingLogDir.mkdir()).isTrue();
+ File logsListFile =
+ new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ logsListFile.createNewFile();
+ writeToFile(logsListFile, existingContent);
+ boolean success = false;
+
+ try (InputStream inputStream = asStream(newContent)) {
+ success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+ }
+
+ assertThat(success).isTrue();
+ assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(existingLogDir.getCanonicalPath());
+ assertThat(readAsString(logsListFile)).isEqualTo(newContent);
+ }
+
+ private static InputStream asStream(String string) throws IOException {
+ return new ByteArrayInputStream(string.getBytes());
+ }
+
+ private static String readAsString(File file) throws IOException {
+ return new String(new FileInputStream(file).readAllBytes());
+ }
+
+ private static void writeToFile(File file, String string) throws IOException {
+ try (OutputStream out = new FileOutputStream(file)) {
+ out.write(string.getBytes());
+ }
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
new file mode 100644
index 0000000..3e670d4
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
@@ -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.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link DataStore}. */
+@RunWith(JUnit4.class)
+public class DataStoreTest {
+
+ private File mTempFile;
+ private DataStore mDataStore;
+
+ @Before
+ public void setUp() throws IOException {
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDataStore_propertyFileCreatedSuccessfully() {
+ assertThat(mTempFile.exists()).isTrue();
+ assertThat(mDataStore.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testDataStore_propertySet() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+
+ @Test
+ public void testDataStore_propertyStore() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+ mDataStore.store();
+
+ mDataStore.clear();
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.load();
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
new file mode 100644
index 0000000..0b65e3c
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
@@ -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.ct;
+
+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.when;
+
+import android.app.DownloadManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for the {@link DownloadHelper}. */
+@RunWith(JUnit4.class)
+public class DownloadHelperTest {
+
+ @Mock private DownloadManager mDownloadManager;
+
+ private DownloadHelper mDownloadHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mDownloadHelper = new DownloadHelper(mDownloadManager);
+ }
+
+ @Test
+ public void testDownloadHelper_scheduleDownload() {
+ long downloadId = 666;
+ when(mDownloadManager.enqueue(any())).thenReturn(downloadId);
+
+ assertThat(mDownloadHelper.startDownload("http://test.org")).isEqualTo(downloadId);
+ }
+
+ @Test
+ public void testDownloadHelper_wrongUri() {
+ when(mDownloadManager.enqueue(any())).thenReturn(666L);
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mDownloadHelper.startDownload("not_a_uri"));
+ }
+}
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 2f1737f..33de139 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -47,7 +47,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 32ae54f..52f301a 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -33,13 +33,13 @@
],
libs: [
"androidx.annotation_annotation",
- "framework-bluetooth",
- "framework-connectivity",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-connectivity.impl",
"error_prone_annotations",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
- "framework-statsd",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"modules-utils-build",
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index fc91e0c..57e3ec9 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -13,7 +13,6 @@
rustlibs: [
"libbinder_rs",
"libjni_legacy",
- "liblazy_static",
"liblog_rust",
"liblogger",
"libnum_traits",
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 421fe7e..9add6df 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -21,12 +21,11 @@
use jni::signature::TypeSignature;
use jni::sys::{jbyteArray, jint, jlong, jvalue};
use jni::{JNIEnv, JavaVM};
-use lazy_static::lazy_static;
use log::{debug, error, info};
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicI64, Ordering},
- Arc, Mutex,
+ Arc, LazyLock, Mutex,
};
/// Macro capturing the name of the function calling this macro.
@@ -51,11 +50,9 @@
}};
}
-lazy_static! {
- static ref HANDLE_MAPPING: Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>> =
- Mutex::new(HashMap::new());
- static ref HANDLE_RN: AtomicI64 = AtomicI64::new(0);
-}
+static HANDLE_MAPPING: LazyLock<Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>>> =
+ LazyLock::new(|| Mutex::new(HashMap::new()));
+static HANDLE_RN: AtomicI64 = AtomicI64::new(0);
fn generate_platform_handle() -> i64 {
HANDLE_RN.fetch_add(1, Ordering::SeqCst)
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 47b9e31..f784b8e 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -30,9 +30,9 @@
srcs: [],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
"framework-annotations-lib",
],
compile_multilib: "both",
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 779f354..787e94e 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -51,14 +51,15 @@
],
libs: [
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
// TODO: use framework-tethering-pre-jarjar when it is separated from framework-tethering
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
"service-nearby-pre-jarjar",
+ "service-networksecurity-pre-jarjar",
"service-thread-pre-jarjar",
service_remoteauth_pre_jarjar_lib,
"ServiceConnectivityResources",
@@ -104,7 +105,6 @@
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
- ":framework-connectivity-t-mdns-standalone-build-sources",
":service-mdns-droidstubs",
],
exclude_srcs: [
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 450f380..9cca078 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -39,12 +39,9 @@
uint32_t poll_ms) {
// Always schedule another run of ourselves to recursively poll periodically.
// The task runner is sequential so these can't run on top of each other.
- runner->PostDelayedTask([=]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
+ runner->PostDelayedTask([=, this]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
- if (mMutex.try_lock()) {
- ConsumeAllLocked();
- mMutex.unlock();
- }
+ ConsumeAll();
}
bool NetworkTracePoller::Start(uint32_t pollMs) {
@@ -76,7 +73,10 @@
return false;
}
- mRingBuffer = std::move(*rb);
+ {
+ std::scoped_lock<std::mutex> block(mBufferMutex);
+ mRingBuffer = std::move(*rb);
+ }
auto res = mConfigurationMap.writeValue(0, true, BPF_ANY);
if (!res.ok()) {
@@ -114,10 +114,14 @@
// Drain remaining events from the ring buffer now that tracing is disabled.
// This prevents the next trace from seeing stale events and allows writing
// the last batch of events to Perfetto.
- ConsumeAllLocked();
+ ConsumeAll();
mTaskRunner.reset();
- mRingBuffer.reset();
+
+ {
+ std::scoped_lock<std::mutex> block(mBufferMutex);
+ mRingBuffer.reset();
+ }
return res.ok();
}
@@ -145,22 +149,20 @@
}
bool NetworkTracePoller::ConsumeAll() {
- std::scoped_lock<std::mutex> lock(mMutex);
- return ConsumeAllLocked();
-}
-
-bool NetworkTracePoller::ConsumeAllLocked() {
- if (mRingBuffer == nullptr) {
- ALOGW("Tracing is not active");
- return false;
- }
-
std::vector<PacketTrace> packets;
- base::Result<int> ret = mRingBuffer->ConsumeAll(
- [&](const PacketTrace& pkt) { packets.push_back(pkt); });
- if (!ret.ok()) {
- ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
- return false;
+ {
+ std::scoped_lock<std::mutex> lock(mBufferMutex);
+ if (mRingBuffer == nullptr) {
+ ALOGW("Tracing is not active");
+ return false;
+ }
+
+ base::Result<int> ret = mRingBuffer->ConsumeAll(
+ [&](const PacketTrace& pkt) { packets.push_back(pkt); });
+ if (!ret.ok()) {
+ ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
+ return false;
+ }
}
ATRACE_INT("NetworkTracePackets", packets.size());
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
index 092ab64..72fa66e 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
@@ -50,7 +50,7 @@
bool Stop() EXCLUDES(mMutex);
// Consumes all available events from the ringbuffer.
- bool ConsumeAll() EXCLUDES(mMutex);
+ bool ConsumeAll() EXCLUDES(mBufferMutex);
private:
// Poll the ring buffer for new data and schedule another run of ourselves
@@ -59,15 +59,19 @@
// and thus a deadlock while resetting the TaskRunner. The runner pointer is
// always valid within tasks run by that runner.
void PollAndSchedule(perfetto::base::TaskRunner* runner, uint32_t poll_ms);
- bool ConsumeAllLocked() REQUIRES(mMutex);
// Record sparse iface stats via atrace. This queries the per-iface stats maps
// for any iface present in the vector of packets. This is inexact, but should
// have sufficient coverage given these are cumulative counters.
- void TraceIfaces(const std::vector<PacketTrace>& packets) REQUIRES(mMutex);
+ static void TraceIfaces(const std::vector<PacketTrace>& packets);
std::mutex mMutex;
+ // The mBufferMutex protects the ring buffer. This allows separate protected
+ // access of mTaskRunner in Stop (to terminate) and mRingBuffer in ConsumeAll.
+ // Without this separation, Stop() can deadlock.
+ std::mutex mBufferMutex;
+
// Records the number of successfully started active sessions so that only the
// first active session attempts setup and only the last cleans up. Note that
// the session count will remain zero if Start fails. It is expected that Stop
@@ -78,10 +82,10 @@
uint32_t mPollMs GUARDED_BY(mMutex);
// The function to process PacketTrace, typically a Perfetto sink.
- EventSink mCallback GUARDED_BY(mMutex);
+ const EventSink mCallback;
// The BPF ring buffer handle.
- std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mMutex);
+ std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mBufferMutex);
// The packet tracing config map (really a 1-element array).
BpfMap<uint32_t, bool> mConfigurationMap GUARDED_BY(mMutex);
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 1ac2f6e..5d23fdc 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -28,6 +28,7 @@
import com.android.server.ethernet.EthernetService;
import com.android.server.ethernet.EthernetServiceImpl;
import com.android.server.nearby.NearbyService;
+import com.android.server.net.ct.CertificateTransparencyService;
import com.android.server.thread.ThreadNetworkService;
/**
@@ -43,6 +44,7 @@
private final NearbyService mNearbyService;
private final EthernetServiceImpl mEthernetServiceImpl;
private final ThreadNetworkService mThreadNetworkService;
+ private final CertificateTransparencyService mCertificateTransparencyService;
public ConnectivityServiceInitializer(Context context) {
super(context);
@@ -55,6 +57,7 @@
mNsdService = createNsdService(context);
mNearbyService = createNearbyService(context);
mThreadNetworkService = createThreadNetworkService(context);
+ mCertificateTransparencyService = createCertificateTransparencyService(context);
}
@Override
@@ -111,6 +114,10 @@
if (mThreadNetworkService != null) {
mThreadNetworkService.onBootPhase(phase);
}
+
+ if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
+ mCertificateTransparencyService.onBootPhase(phase);
+ }
}
/**
@@ -186,4 +193,13 @@
}
return new ThreadNetworkService(context);
}
+
+ /** Return CertificateTransparencyService instance if enable, otherwise null. */
+ @Nullable
+ private CertificateTransparencyService createCertificateTransparencyService(
+ final Context context) {
+ return SdkLevel.isAtLeastV() && CertificateTransparencyService.enabled(context)
+ ? new CertificateTransparencyService(context)
+ : null;
+ }
}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 64624ae..0adb290 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -93,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.DnsUtils;
import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InetAddressUtils;
import com.android.net.module.util.PermissionUtils;
@@ -768,7 +769,7 @@
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);
+ subtypeMap.put(DnsUtils.toDnsUpperCase(subtype), subtype);
}
return new ArraySet<>(subtypeMap.values());
}
@@ -1935,8 +1936,27 @@
mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
.setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
- .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
- mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
+ .setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
+ .setIsCachedServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_CACHED_SERVICES_REMOVAL))
+ .setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
+ MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
+ MdnsFeatureFlags.DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS))
+ .setOverrideProvider(new MdnsFeatureFlags.FlagOverrideProvider() {
+ @Override
+ public boolean isForceEnabledForTest(@NonNull String flag) {
+ return mDeps.isFeatureEnabled(
+ mContext,
+ FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag);
+ }
+
+ @Override
+ public int getIntValueForTest(@NonNull String flag, int defaultValue) {
+ return mDeps.getDeviceConfigPropertyInt(
+ FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag, defaultValue);
+ }
+ })
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
@@ -2003,6 +2023,14 @@
}
/**
+ * @see DeviceConfigUtils#getDeviceConfigPropertyInt
+ */
+ public int getDeviceConfigPropertyInt(String feature, int defaultValue) {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ NAMESPACE_TETHERING, feature, defaultValue);
+ }
+
+ /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index 54943c7..f55db93 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -24,6 +24,7 @@
import android.util.Pair;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -193,7 +194,7 @@
// Ignore any PTR records that don't match the current query.
if (!CollectionUtils.any(questions,
q -> q instanceof MdnsPointerRecord
- && MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ && DnsUtils.equalsDnsLabelIgnoreDnsCase(
q.getName(), ptrRecord.getName()))) {
continue;
}
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 42efcac..9c52eca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -41,6 +41,7 @@
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.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -272,7 +273,7 @@
return true;
}
// Check if it conflicts with the default hostname.
- return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
+ return DnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
}
private void updateRegistrationUntilNoConflict(
@@ -436,8 +437,8 @@
continue;
}
final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
- if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
- && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+ if (DnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+ && DnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
other.getServiceType())) {
return mPendingRegistrations.keyAt(i);
}
@@ -449,7 +450,7 @@
* Get the ID of a conflicting registration due to host, or -1 if none.
*
* <p>If there's already another registration with the same hostname requested by another
- * user, this is a conflict.
+ * UID, this is a conflict.
*
* <p>If there're two registrations both containing address records using the same hostname,
* this is a conflict.
@@ -463,13 +464,13 @@
final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
final int otherServiceId = mPendingRegistrations.keyAt(i);
if (clientUid != otherRegistration.mClientUid
- && MdnsUtils.equalsIgnoreDnsCase(
+ && DnsUtils.equalsIgnoreDnsCase(
info.getHostname(), otherInfo.getHostname())) {
return otherServiceId;
}
if (!info.getHostAddresses().isEmpty()
&& !otherInfo.getHostAddresses().isEmpty()
- && MdnsUtils.equalsIgnoreDnsCase(
+ && DnsUtils.equalsIgnoreDnsCase(
info.getHostname(), otherInfo.getHostname())) {
return otherServiceId;
}
@@ -849,7 +850,7 @@
sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
continue;
}
- priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+ priorities.put(DnsUtils.toDnsUpperCase(priorityAndType[1]), priority);
}
return priorities;
}
@@ -995,7 +996,7 @@
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
final Integer mapPriority = mServiceTypeToOffloadPriority.get(
- MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+ DnsUtils.toDnsUpperCase(nsdServiceInfo.getServiceType()));
// Higher values of priority are less prioritized
final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
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 0ab7a76..b16d8bd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -30,6 +32,7 @@
import androidx.annotation.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -66,8 +69,8 @@
public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
@NonNull MdnsServiceTypeClient client) {
- final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
- final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+ final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+ final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
socketKey);
clients.put(perSocketServiceType, client);
}
@@ -75,18 +78,18 @@
@Nullable
public MdnsServiceTypeClient get(
@NonNull String serviceType, @NonNull SocketKey socketKey) {
- final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
- final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+ final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+ final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
socketKey);
return clients.getOrDefault(perSocketServiceType, null);
}
public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
- final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+ final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
final List<MdnsServiceTypeClient> list = new ArrayList<>();
for (int i = 0; i < clients.size(); i++) {
final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
- if (dnsLowerServiceType.equals(perSocketServiceType.first)) {
+ if (dnsUpperServiceType.equals(perSocketServiceType.first)) {
list.add(clients.valueAt(i));
}
}
@@ -133,13 +136,20 @@
this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
}
- private static class DiscoveryExecutor implements Executor {
+ /**
+ * A utility class to generate a handler, optionally with a looper, and to run functions on the
+ * newly created handler.
+ */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ static class DiscoveryExecutor implements Executor {
private final HandlerThread handlerThread;
@GuardedBy("pendingTasks")
@Nullable private Handler handler;
+ // Store pending tasks and associated delay time. Each Pair represents a pending task
+ // (first) and its delay time (second).
@GuardedBy("pendingTasks")
- @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+ @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
DiscoveryExecutor(@Nullable Looper defaultLooper) {
if (defaultLooper != null) {
@@ -153,8 +163,8 @@
protected void onLooperPrepared() {
synchronized (pendingTasks) {
handler = new Handler(getLooper());
- for (Runnable pendingTask : pendingTasks) {
- handler.post(pendingTask);
+ for (Pair<Runnable, Long> pendingTask : pendingTasks) {
+ handler.postDelayed(pendingTask.first, pendingTask.second);
}
pendingTasks.clear();
}
@@ -176,16 +186,20 @@
@Override
public void execute(Runnable function) {
+ executeDelayed(function, 0L /* delayMillis */);
+ }
+
+ public void executeDelayed(Runnable function, long delayMillis) {
final Handler handler;
synchronized (pendingTasks) {
if (this.handler == null) {
- pendingTasks.add(function);
+ pendingTasks.add(Pair.create(function, delayMillis));
return;
} else {
handler = this.handler;
}
}
- handler.post(function);
+ handler.postDelayed(function, delayMillis);
}
void shutDown() {
@@ -287,6 +301,17 @@
serviceTypeClient.notifySocketDestroyed();
executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
perSocketServiceTypeClients.remove(serviceTypeClient);
+ // The cached services may not be reliable after the socket is disconnected,
+ // the service type client won't receive any updates for them. Therefore,
+ // remove these cached services after exceeding the retention time
+ // (currently 10s) if no service type client requires them.
+ if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
+ final MdnsServiceCache.CacheKey cacheKey =
+ serviceTypeClient.getCacheKey();
+ discoveryExecutor.executeDelayed(
+ () -> handleRemoveCachedServices(cacheKey),
+ mdnsFeatureFlags.getCachedServicesRetentionTime());
+ }
}
});
}
@@ -323,6 +348,42 @@
// of the service type clients.
executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
perSocketServiceTypeClients.remove(serviceTypeClient);
+ // The cached services may not be reliable after the socket is disconnected, the
+ // service type client won't receive any updates for them. Therefore, remove these
+ // cached services after exceeding the retention time (currently 10s) if no service
+ // type client requires them.
+ // Note: This removal is only called if the requested socket is still active for
+ // other requests. If the requested socket is no longer needed after the listener
+ // is unregistered, SocketCreationCallback#onSocketDestroyed callback will remove
+ // both the service type client and cached services there.
+ //
+ // List some multiple listener cases for the cached service removal flow.
+ //
+ // Case 1 - Same service type, different network requests
+ // - Register Listener A (service type X, requesting all networks: Y and Z)
+ // - Create service type clients X-Y and X-Z
+ // - Register Listener B (service type X, requesting network Y)
+ // - Reuse service type client X-Y
+ // - Unregister Listener A
+ // - Socket destroyed on network Z; remove the X-Z client. Unregister the listener
+ // from the X-Y client and keep it, as it's still being used by Listener B.
+ // - Remove cached services associated with the X-Z client after 10 seconds.
+ //
+ // Case 2 - Different service types, same network request
+ // - Register Listener A (service type X, requesting network Y)
+ // - Create service type client X-Y
+ // - Register Listener B (service type Z, requesting network Y)
+ // - Create service type client Z-Y
+ // - Unregister Listener A
+ // - No socket is destroyed because network Y is still being used by Listener B.
+ // - Unregister the listener from the X-Y client, then remove it.
+ // - Remove cached services associated with the X-Y client after 10 seconds.
+ if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
+ final MdnsServiceCache.CacheKey cacheKey = serviceTypeClient.getCacheKey();
+ discoveryExecutor.executeDelayed(
+ () -> handleRemoveCachedServices(cacheKey),
+ mdnsFeatureFlags.getCachedServicesRetentionTime());
+ }
}
}
if (perSocketServiceTypeClients.isEmpty()) {
@@ -367,6 +428,26 @@
}
}
+ private void handleRemoveCachedServices(@NonNull MdnsServiceCache.CacheKey cacheKey) {
+ // Check if there is an active service type client that requires the cached services. If so,
+ // do not remove associated services from cache.
+ for (MdnsServiceTypeClient client : getMdnsServiceTypeClient(cacheKey.mSocketKey)) {
+ if (client.getCacheKey().equals(cacheKey)) {
+ // Found a client that has same CacheKey.
+ return;
+ }
+ }
+ sharedLog.log("Remove cached services for " + cacheKey);
+ // No client has same CacheKey. Remove associated services.
+ getServiceCache().removeServices(cacheKey);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ MdnsServiceCache getServiceCache() {
+ return serviceCache;
+ }
+
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
@@ -389,12 +470,18 @@
*/
public void dump(PrintWriter pw) {
discoveryExecutor.checkAndRunOnHandlerThread(() -> {
- pw.println();
+ pw.println("Clients:");
// Dump ServiceTypeClients
for (MdnsServiceTypeClient serviceTypeClient
: perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
serviceTypeClient.dump(pw);
}
+ pw.println();
+ // Dump ServiceCache
+ pw.println("Cached services:");
+ if (serviceCache != null) {
+ serviceCache.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 c264f25..4e27fef 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -67,6 +67,28 @@
*/
public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
+ /**
+ * A feature flag to avoid advertising empty TXT records, as per RFC 6763 6.1.
+ */
+ public static final String NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS =
+ "nsd_avoid_advertising_empty_txt_records";
+
+ /**
+ * A feature flag to control whether the cached services removal should be enabled.
+ * The removal will be triggered if the retention time has elapsed after all listeners have been
+ * unregistered from the service type client or the interface has been destroyed.
+ */
+ public static final String NSD_CACHED_SERVICES_REMOVAL = "nsd_cached_services_removal";
+
+ /**
+ * A feature flag to control the retention time for cached services.
+ *
+ * <p> Making the retention time configurable allows for testing and future adjustments.
+ */
+ public static final String NSD_CACHED_SERVICES_RETENTION_TIME =
+ "nsd_cached_services_retention_time";
+ public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -91,6 +113,15 @@
// Flag for query with known-answer
public final boolean mIsQueryWithKnownAnswerEnabled;
+ // Flag for avoiding advertising empty TXT records
+ public final boolean mAvoidAdvertisingEmptyTxtRecords;
+
+ // Flag for cached services removal
+ public final boolean mIsCachedServicesRemovalEnabled;
+
+ // Retention Time for cached services
+ public final long mCachedServicesRetentionTime;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -102,6 +133,12 @@
* Indicates whether the flag should be force-enabled for testing purposes.
*/
boolean isForceEnabledForTest(@NonNull String flag);
+
+
+ /**
+ * Get the int value of the flag for testing purposes.
+ */
+ int getIntValueForTest(@NonNull String flag, int defaultValue);
}
/**
@@ -112,6 +149,19 @@
}
/**
+ * Get the int value of the flag for testing purposes.
+ *
+ * @return the test int value, or given default value if it is unset or the OverrideProvider
+ * doesn't exist.
+ */
+ private int getIntValueForTest(@NonNull String flag, int defaultValue) {
+ if (mOverrideProvider == null) {
+ return defaultValue;
+ }
+ return mOverrideProvider.getIntValueForTest(flag, defaultValue);
+ }
+
+ /**
* Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
*/
public boolean isUnicastReplyEnabled() {
@@ -142,6 +192,32 @@
}
/**
+ * Indicates whether {@link #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS} is enabled, including for
+ * testing.
+ */
+ public boolean avoidAdvertisingEmptyTxtRecords() {
+ return mAvoidAdvertisingEmptyTxtRecords
+ || isForceEnabledForTest(NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_CACHED_SERVICES_REMOVAL} is enabled, including for testing.
+ */
+ public boolean isCachedServicesRemovalEnabled() {
+ return mIsCachedServicesRemovalEnabled
+ || isForceEnabledForTest(NSD_CACHED_SERVICES_REMOVAL);
+ }
+
+ /**
+ * Get the value which is set to {@link #NSD_CACHED_SERVICES_RETENTION_TIME}, including for
+ * testing.
+ */
+ public long getCachedServicesRetentionTime() {
+ return getIntValueForTest(
+ NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -152,6 +228,9 @@
boolean isUnicastReplyEnabled,
boolean isAggressiveQueryModeEnabled,
boolean isQueryWithKnownAnswerEnabled,
+ boolean avoidAdvertisingEmptyTxtRecords,
+ boolean isCachedServicesRemovalEnabled,
+ long cachedServicesRetentionTime,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -161,6 +240,9 @@
mIsUnicastReplyEnabled = isUnicastReplyEnabled;
mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+ mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+ mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
+ mCachedServicesRetentionTime = cachedServicesRetentionTime;
mOverrideProvider = overrideProvider;
}
@@ -181,6 +263,9 @@
private boolean mIsUnicastReplyEnabled;
private boolean mIsAggressiveQueryModeEnabled;
private boolean mIsQueryWithKnownAnswerEnabled;
+ private boolean mAvoidAdvertisingEmptyTxtRecords;
+ private boolean mIsCachedServicesRemovalEnabled;
+ private long mCachedServicesRetentionTime;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -195,6 +280,9 @@
mIsUnicastReplyEnabled = true; // Default enabled.
mIsAggressiveQueryModeEnabled = false;
mIsQueryWithKnownAnswerEnabled = false;
+ mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
+ mIsCachedServicesRemovalEnabled = false;
+ mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
mOverrideProvider = null;
}
@@ -291,6 +379,36 @@
}
/**
+ * Set whether to avoid advertising empty TXT records.
+ *
+ * @see #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS
+ */
+ public Builder setAvoidAdvertisingEmptyTxtRecords(boolean avoidAdvertisingEmptyTxtRecords) {
+ mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+ return this;
+ }
+
+ /**
+ * Set whether the cached services removal is enabled.
+ *
+ * @see #NSD_CACHED_SERVICES_REMOVAL
+ */
+ public Builder setIsCachedServicesRemovalEnabled(boolean isCachedServicesRemovalEnabled) {
+ mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
+ return this;
+ }
+
+ /**
+ * Set cached services retention time.
+ *
+ * @see #NSD_CACHED_SERVICES_RETENTION_TIME
+ */
+ public Builder setCachedServicesRetentionTime(long cachedServicesRetentionTime) {
+ mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -302,6 +420,9 @@
mIsUnicastReplyEnabled,
mIsAggressiveQueryModeEnabled,
mIsQueryWithKnownAnswerEnabled,
+ mAvoidAdvertisingEmptyTxtRecords,
+ mIsCachedServicesRemovalEnabled,
+ mCachedServicesRetentionTime,
mOverrideProvider);
}
}
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 fcfb15f..c575d40 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -29,7 +29,9 @@
import android.util.ArrayMap;
import android.util.Log;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -225,6 +227,12 @@
Log.wtf(TAG, "No mDns packets to send");
return;
}
+ // Check all packets with the same address
+ if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+ Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+ + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+ return;
+ }
final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
.getAddress() instanceof Inet6Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
index 1239180..a5b8803 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,7 +20,7 @@
import android.text.TextUtils;
import com.android.net.module.util.CollectionUtils;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.ArrayList;
@@ -153,7 +153,7 @@
@Override
public int hashCode() {
return Objects.hash(super.hashCode(),
- Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+ Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(mNextDomain)),
Arrays.hashCode(mTypes));
}
@@ -167,7 +167,7 @@
}
return super.equals(other)
- && MdnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
+ && DnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
((MdnsNsecRecord) other).mNextDomain)
&& Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 6879a64..cf788be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -16,8 +16,8 @@
package com.android.server.connectivity.mdns;
+import com.android.net.module.util.DnsUtils;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -180,7 +180,7 @@
int existingOffset = entry.getKey();
String[] existingLabels = entry.getValue();
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
writePointer(existingOffset);
return;
} else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index e5c90a4..39bf653 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -20,7 +20,7 @@
import androidx.annotation.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -68,7 +68,7 @@
}
public boolean hasSubtype() {
- return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+ return (name != null) && (name.length > 2) && DnsUtils.equalsIgnoreDnsCase(name[1],
MdnsConstants.SUBTYPE_LABEL);
}
@@ -83,7 +83,7 @@
@Override
public int hashCode() {
- return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
+ return (super.hashCode() * 31) + Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(pointer));
}
@Override
@@ -95,7 +95,7 @@
return false;
}
- return super.equals(other) && MdnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
+ return super.equals(other) && DnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
((MdnsPointerRecord) other).pointer);
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index e88947a..4a44fff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -23,8 +23,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.util.ArrayList;
import java.util.Collections;
@@ -114,7 +114,7 @@
private static boolean containsName(@NonNull List<MdnsRecord> records,
@NonNull String[] name) {
return CollectionUtils.any(records,
- r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
+ r -> DnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 3fcf0d4..cfeca5d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -63,17 +63,22 @@
* rescheduling is not necessary.
*/
@Nullable
- public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(long now,
- long minRemainingTtl, long lastSentTime, long sessionId) {
+ public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(
+ long now,
+ long minRemainingTtl,
+ long lastSentTime,
+ long sessionId,
+ int numOfQueriesBeforeBackoff) {
if (mLastScheduledQueryTaskArgs == null) {
return null;
}
- if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff()) {
+ if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
return null;
}
final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime);
+ mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+ numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
return null;
@@ -95,14 +100,18 @@
long minRemainingTtl,
long now,
long lastSentTime,
- long sessionId) {
- final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun();
- final long timeToRun;
- if (mLastScheduledQueryTaskArgs == null) {
+ long sessionId,
+ int queryMode,
+ int numOfQueriesBeforeBackoff,
+ boolean forceEnableBackoff) {
+ final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
+ long timeToRun;
+ if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
} else {
timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- nextRunConfig, now, minRemainingTtl, lastSentTime);
+ nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
+ forceEnableBackoff);
}
mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
minRemainingTtl + now,
@@ -121,10 +130,12 @@
return mLastScheduledQueryTaskArgs;
}
- private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
- QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
+ private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
+ QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+ int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
- if (!queryTaskConfig.shouldUseQueryBackoff()) {
+ if (!(forceEnableBackoff
+ || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
return lastSentTime + baseDelayInMs;
}
if (minRemainingTtl <= 0) {
@@ -133,7 +144,7 @@
return lastSentTime + baseDelayInMs;
}
// If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
- if (lastSentTime < now
+ if (lastSentTime < now && taskArgs != null
&& taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
// Use the original scheduling time if the TTL has not changed, to avoid continuously
// rescheduling to 80% of the remaining TTL as time passes
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 b865319..d464ca7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -25,7 +25,7 @@
import androidx.annotation.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -139,7 +139,7 @@
}
for (int i = 0; i < list1.length; ++i) {
- if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
+ if (!DnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
return false;
}
}
@@ -284,13 +284,13 @@
MdnsRecord otherRecord = (MdnsRecord) other;
- return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+ return DnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
== otherRecord.type);
}
@Override
public int hashCode() {
- return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
+ return Objects.hash(Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(name)), type);
}
/**
@@ -311,7 +311,7 @@
public Key(int recordType, String[] recordName) {
this.recordType = recordType;
- this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
+ this.recordName = DnsUtils.toDnsLabelsUpperCase(recordName);
}
@Override
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 36f3982..c3cb776 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -38,6 +38,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.HexDump;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -226,11 +227,13 @@
/**
* Create a ServiceRegistration with only update the subType.
*/
- ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+ ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes,
+ @NonNull MdnsFeatureFlags featureFlags) {
NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
newServiceInfo.setSubtypes(newSubtypes);
return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
- repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+ repliedServiceCount, sentPacketCount, exiting, isProbing, ttl,
+ featureFlags);
}
/**
@@ -238,7 +241,7 @@
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
- @Nullable Duration ttl) {
+ @Nullable Duration ttl, @NonNull MdnsFeatureFlags featureFlags) {
this.serviceInfo = serviceInfo;
final long nonNameRecordsTtlMillis;
@@ -309,7 +312,8 @@
// Service name is verified unique after probing
true /* cacheFlush */,
nonNameRecordsTtlMillis,
- attrsToTextEntries(serviceInfo.getAttributes())),
+ attrsToTextEntries(
+ serviceInfo.getAttributes(), featureFlags)),
false /* sharedName */);
allRecords.addAll(ptrRecords);
@@ -392,9 +396,10 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+ int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl,
+ @NonNull MdnsFeatureFlags featureFlags) {
this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
- false /* exiting */, true /* isProbing */, ttl);
+ false /* exiting */, true /* isProbing */, ttl, featureFlags);
}
void setProbing(boolean probing) {
@@ -445,7 +450,7 @@
"Service ID must already exist for an update request: " + serviceId);
}
final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
- subtypes);
+ subtypes, mMdnsFeatureFlags);
mServices.put(serviceId, updatedRegistration);
}
@@ -476,7 +481,8 @@
final ServiceRegistration registration = new ServiceRegistration(
mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
- NO_PACKET /* sentPacketCount */, ttl);
+ NO_PACKET /* sentPacketCount */, ttl,
+ mMdnsFeatureFlags);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -494,8 +500,8 @@
}
for (int i = 0; i < mServices.size(); i++) {
final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
- if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
- && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
+ if (DnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+ && DnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
return mServices.keyAt(i);
}
}
@@ -547,8 +553,17 @@
return new MdnsProber.ProbingInfo(serviceId, probingRecords);
}
- private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
- final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
+ private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(
+ @NonNull Map<String, byte[]> attrs, @NonNull MdnsFeatureFlags featureFlags) {
+ final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(
+ attrs.size() == 0 ? 1 : attrs.size());
+ if (featureFlags.avoidAdvertisingEmptyTxtRecords() && attrs.size() == 0) {
+ // As per RFC6763 6.1, empty TXT records are not allowed, but records containing a
+ // single empty String must be treated as equivalent.
+ out.add(new MdnsServiceInfo.TextEntry("", MdnsServiceInfo.TextEntry.VALUE_NONE));
+ return out;
+ }
+
for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
}
@@ -821,7 +836,7 @@
must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
"CNAME" (5), and the record rrclass must match the question qclass unless the
qclass is "ANY" (255) */
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
continue;
}
hasFullyOwnedNameMatch |= !info.isSharedName;
@@ -1232,7 +1247,7 @@
return RecordConflictType.NO_CONFLICT;
}
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
return RecordConflictType.NO_CONFLICT;
}
@@ -1270,7 +1285,7 @@
}
// Different names. There won't be a conflict.
- if (!MdnsUtils.equalsIgnoreDnsCase(
+ if (!DnsUtils.equalsIgnoreDnsCase(
record.getName()[0], registration.serviceInfo.getHostname())) {
return RecordConflictType.NO_CONFLICT;
}
@@ -1351,7 +1366,7 @@
int id = mServices.keyAt(i);
ServiceRegistration service = mServices.valueAt(i);
if (service.exiting) continue;
- if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+ if (DnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
consumer.accept(id, service);
}
}
@@ -1402,7 +1417,8 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
+ existing.repliedServiceCount, existing.sentPacketCount, existing.ttl,
+ mMdnsFeatureFlags);
mServices.put(serviceId, newService);
return makeProbingInfo(serviceId, newService);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index 05ad1be..2957da5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -19,11 +19,14 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
+import android.os.SystemClock;
+import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -111,7 +114,7 @@
* pointer record is already present in the response with the same TTL.
*/
public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
throw new IllegalArgumentException(
"Pointer records for different service names cannot be added");
}
@@ -305,13 +308,13 @@
boolean dropAddressRecords = false;
for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
dropAddressRecords = true;
}
}
for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
dropAddressRecords = true;
}
@@ -426,4 +429,18 @@
return count;
}
+
+ @Override
+ public String toString() {
+ return "Name: " + TextUtils.join(".", serviceName)
+ + ", pointerRecords: " + pointerRecords
+ + ", serviceRecord: " + serviceRecord
+ + ", textRecord: " + textRecord
+ + ", inet4AddressRecords: " + inet4AddressRecords
+ + ", inet6AddressRecords: " + inet6AddressRecords
+ + ", interfaceIndex: " + interfaceIndex
+ + ", network: " + network
+ + ", lastUpdateTime: " + Instant.now().minusMillis(
+ SystemClock.elapsedRealtime() - lastUpdateTime);
+ }
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index b812bb4..52e76ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -22,6 +22,7 @@
import android.util.ArrayMap;
import android.util.Pair;
+import com.android.net.module.util.DnsUtils;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.EOFException;
@@ -49,7 +50,7 @@
List<MdnsResponse> responses, String[] pointer) {
if (responses != null) {
for (MdnsResponse response : responses) {
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
return response;
}
}
@@ -65,7 +66,7 @@
if (serviceRecord == null) {
continue;
}
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
hostName)) {
return response;
}
@@ -318,7 +319,7 @@
if (serviceRecord == null) {
continue;
}
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
if (result == null) {
result = new ArrayList<>(/* initialCapacity= */ responses.size());
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index e9a41d1..22f7a03 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,10 +16,10 @@
package com.android.server.connectivity.mdns;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.toDnsUpperCase;
import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
import static java.lang.Math.min;
@@ -32,6 +32,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -48,17 +49,17 @@
* to their default value (0, false or null).
*/
public class MdnsServiceCache {
- static class CacheKey {
- @NonNull final String mLowercaseServiceType;
+ public static class CacheKey {
+ @NonNull final String mUpperCaseServiceType;
@NonNull final SocketKey mSocketKey;
CacheKey(@NonNull String serviceType, @NonNull SocketKey socketKey) {
- mLowercaseServiceType = toDnsLowerCase(serviceType);
+ mUpperCaseServiceType = toDnsUpperCase(serviceType);
mSocketKey = socketKey;
}
@Override public int hashCode() {
- return Objects.hash(mLowercaseServiceType, mSocketKey);
+ return Objects.hash(mUpperCaseServiceType, mSocketKey);
}
@Override public boolean equals(Object other) {
@@ -68,9 +69,14 @@
if (!(other instanceof CacheKey)) {
return false;
}
- return Objects.equals(mLowercaseServiceType, ((CacheKey) other).mLowercaseServiceType)
+ return Objects.equals(mUpperCaseServiceType, ((CacheKey) other).mUpperCaseServiceType)
&& Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
}
+
+ @Override
+ public String toString() {
+ return "CacheKey{ ServiceType=" + mUpperCaseServiceType + ", " + mSocketKey + " }";
+ }
}
/**
* A map of cached services. Key is composed of service type and socket. Value is the list of
@@ -233,6 +239,21 @@
}
/**
+ * Remove services which matches the given type and socket.
+ *
+ * @param cacheKey the target CacheKey.
+ */
+ public void removeServices(@NonNull CacheKey cacheKey) {
+ ensureRunningOnHandlerThread(mHandler);
+ // Remove all services
+ if (mCachedServices.remove(cacheKey) == null) {
+ return;
+ }
+ // Update the next expiration check time if services are removed.
+ mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+ }
+
+ /**
* Register a callback to listen to service expiration.
*
* <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
@@ -338,6 +359,22 @@
mNextExpirationTime = getNextExpirationTime(now);
}
+ /**
+ * Dump ServiceCache state.
+ */
+ public void dump(PrintWriter pw, String indent) {
+ ensureRunningOnHandlerThread(mHandler);
+ // IndentingPrintWriter cannot be used on the mDNS stack build. So, manually add an indent.
+ for (int i = 0; i < mCachedServices.size(); i++) {
+ final CacheKey key = mCachedServices.keyAt(i);
+ pw.println(indent + key);
+ for (MdnsResponse response : mCachedServices.valueAt(i)) {
+ pw.println(indent + " Response{ " + response + " }");
+ }
+ pw.println();
+ }
+ }
+
/*** Callbacks for listening service expiration */
public interface ServiceExpiredCallback {
/*** Notify the service is expired */
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 1ec9e39..a16fcf7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -16,8 +16,6 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
@@ -33,7 +31,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
@@ -355,6 +352,13 @@
/** Represents a DNS TXT key-value pair defined by RFC 6763. */
public static final class TextEntry implements Parcelable {
+ /**
+ * The value to use for attributes with no value.
+ *
+ * <p>As per RFC6763 P.16, attributes may have no value, which is different from having an
+ * empty value (which would be an empty byte array).
+ */
+ public static final byte[] VALUE_NONE = null;
public static final Parcelable.Creator<TextEntry> CREATOR =
new Parcelable.Creator<TextEntry>() {
@Override
@@ -389,7 +393,7 @@
// 2. If there is no '=' in a DNS-SD TXT record string, then it is a
// boolean attribute, simply identified as being present, with no value.
if (delimitPos < 0) {
- return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
+ return new TextEntry(new String(textBytes, US_ASCII), VALUE_NONE);
} else if (delimitPos == 0) {
return null;
}
@@ -400,13 +404,13 @@
/** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
public TextEntry(String key, String value) {
- this(key, value == null ? null : value.getBytes(UTF_8));
+ this(key, value == null ? VALUE_NONE : value.getBytes(UTF_8));
}
/** Creates a new {@link TextEntry} with given key and value of a byte array. */
public TextEntry(String key, byte[] value) {
this.key = key;
- this.value = value == null ? null : value.clone();
+ this.value = value == VALUE_NONE ? VALUE_NONE : value.clone();
}
private TextEntry(Parcel in) {
@@ -419,22 +423,26 @@
}
public byte[] getValue() {
- return value == null ? null : value.clone();
+ return value == VALUE_NONE ? VALUE_NONE : value.clone();
}
/** Converts this {@link TextEntry} instance to '=' separated byte array. */
public byte[] toBytes() {
final byte[] keyBytes = key.getBytes(US_ASCII);
- if (value == null) {
+ if (value == VALUE_NONE) {
return keyBytes;
}
return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
}
+ public boolean isEmpty() {
+ return TextUtils.isEmpty(key) && (value == VALUE_NONE || value.length == 0);
+ }
+
/** Converts this {@link TextEntry} instance to '=' separated string. */
@Override
public String toString() {
- if (value == null) {
+ if (value == VALUE_NONE) {
return key;
}
return key + "=" + new String(value, UTF_8);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index 0d6a9ec..fd716d2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -20,7 +20,7 @@
import androidx.annotation.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -151,7 +151,7 @@
public int hashCode() {
return (super.hashCode() * 31)
+ Objects.hash(servicePriority, serviceWeight,
- Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
+ Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(serviceHost)),
servicePort);
}
@@ -168,7 +168,7 @@
return super.equals(other)
&& (servicePriority == otherRecord.servicePriority)
&& (serviceWeight == otherRecord.serviceWeight)
- && MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
+ && DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
&& (servicePort == otherRecord.servicePort);
}
}
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 b3bdbe0..a5dd536 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
@@ -33,6 +34,7 @@
import androidx.annotation.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -120,11 +122,11 @@
* @return true if the service name was not discovered before.
*/
boolean setServiceDiscovered(@NonNull String serviceName) {
- return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+ return discoveredServiceNames.add(DnsUtils.toDnsUpperCase(serviceName));
}
void unsetServiceDiscovered(@NonNull String serviceName) {
- discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+ discoveredServiceNames.remove(DnsUtils.toDnsUpperCase(serviceName));
}
}
@@ -147,7 +149,8 @@
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
- getExistingServices());
+ getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+ socketKey);
executor.submit(queryTask);
break;
}
@@ -178,7 +181,10 @@
minRemainingTtl,
now,
lastSentTime,
- sentResult.taskArgs.sessionId
+ sentResult.taskArgs.sessionId,
+ searchOptions.getQueryMode(),
+ searchOptions.numOfQueriesBeforeBackoff(),
+ false /* forceEnableBackoff */
);
dependencies.sendMessageDelayed(
handler,
@@ -303,8 +309,8 @@
serviceCache.unregisterServiceExpiredCallback(cacheKey);
}
- private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
- @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+ private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
+ @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
String[] hostName = null;
int port = 0;
if (response.hasServiceRecord()) {
@@ -351,7 +357,7 @@
textEntries,
response.getInterfaceIndex(),
response.getNetwork(),
- now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
+ now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
}
private List<MdnsResponse> getExistingServices() {
@@ -380,8 +386,8 @@
if (existingInfo == null) {
for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
- final MdnsServiceInfo info =
- buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
+ final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse(
+ existingResponse, serviceTypeLabels, clock.elapsedRealtime());
listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
if (existingResponse.isComplete()) {
@@ -392,14 +398,13 @@
}
// Remove the next scheduled periodical task.
removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
- // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
- // interested anymore.
- final QueryTaskConfig taskConfig = new QueryTaskConfig(
- searchOptions.getQueryMode(),
- searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
- searchOptions.numOfQueriesBeforeBackoff(),
- socketKey);
+ final boolean forceEnableBackoff =
+ (searchOptions.getQueryMode() == AGGRESSIVE_QUERY_MODE && hadReply);
+ // Keep the latest scheduled run for rescheduling if there is a service in the cache.
+ if (!(forceEnableBackoff)) {
+ mdnsQueryScheduler.cancelScheduledRun();
+ }
+ final QueryTaskConfig taskConfig = new QueryTaskConfig(searchOptions.getQueryMode());
final long now = clock.elapsedRealtime();
if (lastSentTime == 0) {
lastSentTime = now;
@@ -412,7 +417,10 @@
minRemainingTtl,
now,
lastSentTime,
- currentSessionId
+ currentSessionId,
+ searchOptions.getQueryMode(),
+ searchOptions.numOfQueriesBeforeBackoff(),
+ forceEnableBackoff
);
dependencies.sendMessageDelayed(
handler,
@@ -424,7 +432,8 @@
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
- getExistingServices());
+ getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+ socketKey);
executor.submit(queryTask);
}
@@ -447,6 +456,14 @@
return executor;
}
+ /**
+ * Get the cache key for this service type client.
+ */
+ @NonNull
+ public MdnsServiceCache.CacheKey getCacheKey() {
+ return cacheKey;
+ }
+
private void removeScheduledTask() {
dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
sharedLog.log("Remove EVENT_START_QUERYTASK"
@@ -458,7 +475,7 @@
@NonNull MdnsSearchOptions options) {
final boolean matchesInstanceName = options.getResolveInstanceName() == null
// DNS is case-insensitive, so ignore case in the comparison
- || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+ || DnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
response.getServiceInstanceName());
// If discovery is requiring some subtypes, the response must have one that matches a
@@ -468,7 +485,7 @@
final boolean matchesSubtype = options.getSubtypes().size() == 0
|| CollectionUtils.any(options.getSubtypes(), requiredSub ->
CollectionUtils.any(responseSubtypes, actualSub ->
- MdnsUtils.equalsIgnoreDnsCase(
+ DnsUtils.equalsIgnoreDnsCase(
MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
return matchesInstanceName && matchesSubtype;
@@ -535,7 +552,8 @@
final long minRemainingTtl = getMinRemainingTtl(now);
MdnsQueryScheduler.ScheduledQueryTaskArgs args =
mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
- lastSentTime, currentSessionId + 1);
+ lastSentTime, currentSessionId + 1,
+ searchOptions.numOfQueriesBeforeBackoff());
if (args != null) {
removeScheduledTask();
dependencies.sendMessageDelayed(
@@ -561,7 +579,7 @@
if (response.getServiceInstanceName() != null) {
listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
- response, serviceTypeLabels);
+ response, serviceTypeLabels, clock.elapsedRealtime());
if (response.isComplete()) {
sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
listener.onServiceRemoved(serviceInfo);
@@ -605,8 +623,8 @@
+ " %b, responseIsComplete: %b",
serviceInstanceName, newInCache, serviceBecomesComplete,
response.isComplete()));
- MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+ final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+ response, serviceTypeLabels, clock.elapsedRealtime());
for (int i = 0; i < listeners.size(); i++) {
// If a service stops matching the options (currently can only happen if it loses a
@@ -658,7 +676,7 @@
continue;
}
if (CollectionUtils.any(resolveResponses,
- r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+ r -> DnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
continue;
}
MdnsResponse knownResponse =
@@ -723,15 +741,21 @@
private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
private final List<MdnsResponse> existingServices = new ArrayList<>();
+ private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+ private final SocketKey socketKey;
QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
- @NonNull Collection<MdnsResponse> existingServices) {
+ @NonNull Collection<MdnsResponse> existingServices,
+ boolean onlyUseIpv6OnIpv6OnlyNetworks,
+ @NonNull SocketKey socketKey) {
this.taskArgs = taskArgs;
this.servicesToResolve.addAll(servicesToResolve);
this.subtypes.addAll(subtypes);
this.sendDiscoveryQueries = sendDiscoveryQueries;
this.existingServices.addAll(existingServices);
+ this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+ this.socketKey = socketKey;
}
@Override
@@ -745,8 +769,8 @@
subtypes,
taskArgs.config.expectUnicastResponse,
taskArgs.config.transactionId,
- taskArgs.config.socketKey,
- taskArgs.config.onlyUseIpv6OnIpv6OnlyNetworks,
+ socketKey,
+ onlyUseIpv6OnIpv6OnlyNetworks,
sendDiscoveryQueries,
servicesToResolve,
clock,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 9cfcba1..17e5b31 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -28,7 +28,9 @@
import android.util.Log;
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.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -249,6 +251,12 @@
Log.wtf(TAG, "No mDns packets to send");
return;
}
+ // Check all packets with the same address
+ if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+ Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+ + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+ return;
+ }
final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
.getAddress() instanceof Inet4Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 92cf324..77d1d7a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -89,6 +89,13 @@
}
}
+ private boolean isEmpty() {
+ return entries == null || entries.size() == 0
+ // RFC6763 6.1 indicates that a TXT record with a single zero byte is equivalent to
+ // an empty record.
+ || (entries.size() == 1 && entries.get(0).isEmpty());
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -105,7 +112,7 @@
@Override
public int hashCode() {
- return (super.hashCode() * 31) + Objects.hash(entries);
+ return (super.hashCode() * 31) + (isEmpty() ? 0 : Objects.hash(entries));
}
@Override
@@ -116,7 +123,19 @@
if (!(other instanceof MdnsTextRecord)) {
return false;
}
-
- return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
+ if (!super.equals(other)) {
+ return false;
+ }
+ // As per RFC6763 6.1: DNS-SD clients MUST treat the following as equivalent:
+ // - A TXT record containing a single zero byte.
+ // - An empty (zero-length) TXT record. (This is not strictly legal, but should one be
+ // received, it should be interpreted as the same as a single empty string.)
+ // - No TXT record
+ // Ensure that empty TXT records are considered equal, so that they are not considered
+ // conflicting for example.
+ if (isEmpty() && ((MdnsTextRecord) other).isEmpty()) {
+ return true;
+ }
+ return Objects.equals(entries, ((MdnsTextRecord) other).entries);
}
}
\ No newline at end of file
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 0894166..d2cd463 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -19,9 +19,6 @@
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;
/**
@@ -51,9 +48,6 @@
static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
private final boolean alwaysAskForUnicastResponse =
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
- private final int queryMode;
- final boolean onlyUseIpv6OnIpv6OnlyNetworks;
- private final int numOfQueriesBeforeBackoff;
@VisibleForTesting
final int transactionId;
@VisibleForTesting
@@ -64,16 +58,11 @@
final long delayUntilNextTaskWithoutBackoffMs;
private final boolean isFirstBurst;
private final long queryCount;
- @NonNull
- final SocketKey socketKey;
- QueryTaskConfig(@NonNull QueryTaskConfig other, long queryCount, int transactionId,
+ QueryTaskConfig(long queryCount, int transactionId,
boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
int queriesPerBurst, int timeBetweenBurstsInMs,
long delayUntilNextTaskWithoutBackoffMs) {
- this.queryMode = other.queryMode;
- this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
- this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
this.transactionId = transactionId;
this.expectUnicastResponse = expectUnicastResponse;
this.queriesPerBurst = queriesPerBurst;
@@ -82,27 +71,20 @@
this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
this.isFirstBurst = isFirstBurst;
this.queryCount = queryCount;
- this.socketKey = other.socketKey;
}
- QueryTaskConfig(int queryMode,
- boolean onlyUseIpv6OnIpv6OnlyNetworks,
- int numOfQueriesBeforeBackoff,
- @Nullable SocketKey socketKey) {
- this.queryMode = queryMode;
- this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
- this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
+ QueryTaskConfig(int queryMode) {
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.queryMode == AGGRESSIVE_QUERY_MODE) {
+ if (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) {
+ } else if (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.
@@ -116,12 +98,11 @@
this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
- this.socketKey = socketKey;
this.queryCount = 0;
}
long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
- boolean isLastQueryInBurst) {
+ boolean isLastQueryInBurst, int queryMode) {
if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
return 0;
}
@@ -133,7 +114,7 @@
: TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
- boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
+ boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst, int queryMode) {
if (!isLastQueryInBurst) {
return false;
}
@@ -143,7 +124,7 @@
return alwaysAskForUnicastResponse;
}
- int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
+ int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst, int queryMode) {
if (!isLastQueryInBurst) {
return timeBetweenBurstsInMs;
}
@@ -155,7 +136,7 @@
/**
* Get new QueryTaskConfig for next run.
*/
- public QueryTaskConfig getConfigForNextRun() {
+ public QueryTaskConfig getConfigForNextRun(int queryMode) {
long newQueryCount = queryCount + 1;
int newTransactionId = transactionId + 1;
if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
@@ -177,16 +158,18 @@
}
}
- return new QueryTaskConfig(this, newQueryCount, newTransactionId,
- getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
- newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
- getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
+ return new QueryTaskConfig(newQueryCount, newTransactionId,
+ getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
+ newBurstCounter, newQueriesPerBurst,
+ getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
+ getDelayUntilNextTaskWithoutBackoff(
+ isFirstQueryInBurst, isLastQueryInBurst, queryMode));
}
/**
* Determine if the query backoff should be used.
*/
- public boolean shouldUseQueryBackoff() {
+ public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
// Don't enable backoff mode during the burst or in the first burst
if (burstCounter != 0 || isFirstBurst) {
return false;
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 3c11a24..8745941 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
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns.util;
+import static com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
import android.annotation.NonNull;
@@ -34,6 +36,7 @@
import java.io.IOException;
import java.net.DatagramPacket;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
@@ -55,18 +58,17 @@
private MdnsUtils() { }
/**
- * Convert the string to DNS case-insensitive lowercase
+ * Compare labels a equals b or a is suffix of b.
*
- * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
- * their unaccented counterparts. So the "DNS lowercase" should be if character is A-Z then they
- * transform into a-z. Otherwise, they are kept as-is.
+ * @param a the type or subtype.
+ * @param b the base type
*/
- public static String toDnsLowerCase(@NonNull String string) {
- final char[] outChars = new char[string.length()];
- for (int i = 0; i < string.length(); i++) {
- outChars[i] = toDnsLowerCase(string.charAt(i));
- }
- return new String(outChars);
+ public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
+ @NonNull String[] b) {
+ return equalsDnsLabelIgnoreDnsCase(a, b)
+ || ((b.length == (a.length + 2))
+ && equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(a, b));
}
/**
@@ -80,70 +82,6 @@
}
}
- /**
- * Convert the array of labels to DNS case-insensitive lowercase.
- */
- public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
- final String[] outStrings = new String[labels.length];
- for (int i = 0; i < labels.length; ++i) {
- outStrings[i] = toDnsLowerCase(labels[i]);
- }
- return outStrings;
- }
-
- /**
- * Compare two strings by DNS case-insensitive lowercase.
- */
- 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))) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Compare two set of DNS labels by DNS case-insensitive lowercase.
- */
- public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
- if (a == b) {
- return true;
- }
- int length = a.length;
- if (b.length != length) {
- return false;
- }
- for (int i = 0; i < length; i++) {
- if (!equalsIgnoreDnsCase(a[i], b[i])) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Compare labels a equals b or a is suffix of b.
- *
- * @param a the type or subtype.
- * @param b the base type
- */
- public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
- @NonNull String[] b) {
- return MdnsUtils.equalsDnsLabelIgnoreDnsCase(a, b)
- || ((b.length == (a.length + 2))
- && MdnsUtils.equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
- && MdnsRecord.labelsAreSuffix(a, b));
- }
-
- private static char toDnsLowerCase(char a) {
- return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a;
- }
-
/*** Ensure that current running thread is same as given handler thread */
public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
if (!isRunningOnHandlerThread(handler)) {
@@ -361,4 +299,23 @@
return SystemClock.elapsedRealtime();
}
}
+
+ /**
+ * Check all DatagramPackets with the same destination address.
+ */
+ public static boolean checkAllPacketsWithSameAddress(List<DatagramPacket> packets) {
+ // No packet for address check
+ if (packets.isEmpty()) {
+ return true;
+ }
+
+ final InetAddress address =
+ ((InetSocketAddress) packets.get(0).getSocketAddress()).getAddress();
+ for (DatagramPacket packet : packets) {
+ if (!address.equals(((InetSocketAddress) packet.getSocketAddress()).getAddress())) {
+ return false;
+ }
+ }
+ return true;
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
new file mode 100644
index 0000000..b8f6859
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
@@ -0,0 +1,344 @@
+/*
+ * 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.ethernet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.State;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * EthernetInterfaceStateMachine manages the lifecycle of an ethernet-like network interface which
+ * includes managing a NetworkOffer, IpClient, and NetworkAgent as well as making the interface
+ * available as a tethering downstream.
+ *
+ * All methods exposed by this class *must* be called on the Handler thread provided in the
+ * constructor.
+ */
+class EthernetInterfaceStateMachine extends SyncStateMachine {
+ private static final String TAG = EthernetInterfaceStateMachine.class.getSimpleName();
+
+ private static final int CMD_ON_LINK_UP = 1;
+ private static final int CMD_ON_LINK_DOWN = 2;
+ private static final int CMD_ON_NETWORK_NEEDED = 3;
+ private static final int CMD_ON_NETWORK_UNNEEDED = 4;
+ private static final int CMD_ON_IPCLIENT_CREATED = 5;
+
+ private class EthernetNetworkOfferCallback implements NetworkOfferCallback {
+ private final Set<Integer> mRequestIds = new ArraySet<>();
+
+ @Override
+ public void onNetworkNeeded(@NonNull NetworkRequest request) {
+ if (this != mNetworkOfferCallback) {
+ return;
+ }
+
+ mRequestIds.add(request.requestId);
+ if (mRequestIds.size() == 1) {
+ processMessage(CMD_ON_NETWORK_NEEDED);
+ }
+ }
+
+ @Override
+ public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+ if (this != mNetworkOfferCallback) {
+ return;
+ }
+
+ if (!mRequestIds.remove(request.requestId)) {
+ // This can only happen if onNetworkNeeded was not called for a request or if
+ // the requestId changed. Both should *never* happen.
+ Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
+ }
+ if (mRequestIds.isEmpty()) {
+ processMessage(CMD_ON_NETWORK_UNNEEDED);
+ }
+ }
+ }
+
+ private class EthernetIpClientCallback extends IpClientCallbacks {
+ private final ConditionVariable mOnQuitCv = new ConditionVariable(false);
+
+ private void safelyPostOnHandler(Runnable r) {
+ mHandler.post(() -> {
+ if (this != mIpClientCallback) {
+ return;
+ }
+ r.run();
+ });
+ }
+
+ @Override
+ public void onIpClientCreated(IIpClient ipClient) {
+ safelyPostOnHandler(() -> {
+ // TODO: add a SyncStateMachine#processMessage(cmd, obj) overload.
+ processMessage(CMD_ON_IPCLIENT_CREATED, 0, 0,
+ mDependencies.makeIpClientManager(ipClient));
+ });
+ }
+
+ public void waitOnQuit() {
+ if (!mOnQuitCv.block(5_000 /* timeoutMs */)) {
+ Log.wtf(TAG, "Timed out waiting on IpClient to shutdown.");
+ }
+ }
+
+ @Override
+ public void onQuit() {
+ mOnQuitCv.open();
+ }
+ }
+
+ private @Nullable EthernetNetworkOfferCallback mNetworkOfferCallback;
+ private @Nullable EthernetIpClientCallback mIpClientCallback;
+ private @Nullable IpClientManager mIpClient;
+ private final String mIface;
+ private final Handler mHandler;
+ private final Context mContext;
+ private final NetworkCapabilities mCapabilities;
+ private final NetworkProvider mNetworkProvider;
+ private final EthernetNetworkFactory.Dependencies mDependencies;
+ private boolean mLinkUp = false;
+
+ /** Interface is in tethering mode. */
+ private class TetheringState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_LINK_UP:
+ case CMD_ON_LINK_DOWN:
+ // TODO: think about what to do here.
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+ }
+
+ /** Link is down */
+ private class LinkDownState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_LINK_UP:
+ transitionTo(mStoppedState);
+ return HANDLED;
+ case CMD_ON_LINK_DOWN:
+ // do nothing, already in the correct state.
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+ }
+
+ /** Parent states of all states that do not cause a NetworkOffer to be extended. */
+ private class NetworkOfferExtendedState extends State {
+ @Override
+ public void enter() {
+ if (mNetworkOfferCallback != null) {
+ // This should never happen. If it happens anyway, log and move on.
+ Log.wtf(TAG, "Previous NetworkOffer was never retracted");
+ }
+
+ mNetworkOfferCallback = new EthernetNetworkOfferCallback();
+ final NetworkScore defaultScore = new NetworkScore.Builder().build();
+ mNetworkProvider.registerNetworkOffer(defaultScore,
+ new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
+ mNetworkOfferCallback);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_LINK_UP:
+ // do nothing, already in the correct state.
+ return HANDLED;
+ case CMD_ON_LINK_DOWN:
+ transitionTo(mLinkDownState);
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+
+ @Override
+ public void exit() {
+ mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+ mNetworkOfferCallback = null;
+ }
+ }
+
+ /**
+ * Offer is extended but has not been requested.
+ *
+ * StoppedState's sole purpose is to react to a CMD_ON_NETWORK_NEEDED and transition to
+ * StartedState when that happens. Note that StoppedState could be rolled into
+ * NetworkOfferExtendedState. However, keeping the states separate provides some additional
+ * protection by logging a Log.wtf if a CMD_ON_NETWORK_NEEDED is received in an unexpected state
+ * (i.e. StartedState or RunningState). StoppedState is a child of NetworkOfferExtendedState.
+ */
+ private class StoppedState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_NETWORK_NEEDED:
+ transitionTo(mStartedState);
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+ }
+
+ /** Network is needed, starts IpClient and manages its lifecycle */
+ private class StartedState extends State {
+ @Override
+ public void enter() {
+ mIpClientCallback = new EthernetIpClientCallback();
+ mDependencies.makeIpClient(mContext, mIface, mIpClientCallback);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_NETWORK_UNNEEDED:
+ transitionTo(mStoppedState);
+ return HANDLED;
+ case CMD_ON_IPCLIENT_CREATED:
+ mIpClient = (IpClientManager) msg.obj;
+ transitionTo(mRunningState);
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+
+ @Override
+ public void exit() {
+ if (mIpClient != null) {
+ mIpClient.shutdown();
+ // TODO: consider adding a StoppingState and making the shutdown operation
+ // asynchronous.
+ mIpClientCallback.waitOnQuit();
+ }
+ mIpClientCallback = null;
+ }
+ }
+
+ /** IpClient is running, starts provisioning and registers NetworkAgent */
+ private class RunningState extends State {
+
+ }
+
+ private final TetheringState mTetheringState = new TetheringState();
+ private final LinkDownState mLinkDownState = new LinkDownState();
+ private final NetworkOfferExtendedState mOfferExtendedState = new NetworkOfferExtendedState();
+ private final StoppedState mStoppedState = new StoppedState();
+ private final StartedState mStartedState = new StartedState();
+ private final RunningState mRunningState = new RunningState();
+
+ public EthernetInterfaceStateMachine(String iface, Handler handler, Context context,
+ NetworkCapabilities capabilities, NetworkProvider provider,
+ EthernetNetworkFactory.Dependencies deps) {
+ super(TAG + "." + iface, handler.getLooper().getThread());
+
+ mIface = iface;
+ mHandler = handler;
+ mContext = context;
+ mCapabilities = capabilities;
+ mNetworkProvider = provider;
+ mDependencies = deps;
+
+ // Interface lifecycle:
+ // [ LinkDownState ]
+ // |
+ // v
+ // *link comes up*
+ // |
+ // v
+ // [ StoppedState ]
+ // |
+ // v
+ // *network is needed*
+ // |
+ // v
+ // [ StartedState ]
+ // |
+ // v
+ // *IpClient is created*
+ // |
+ // v
+ // [ RunningState ]
+ // |
+ // v
+ // *interface is requested for tethering*
+ // |
+ // v
+ // [TetheringState]
+ //
+ // Tethering mode is special as the interface is configured by Tethering, rather than the
+ // ethernet module.
+ final List<StateInfo> states = new ArrayList<>();
+ states.add(new StateInfo(mTetheringState, null));
+
+ // CHECKSTYLE:OFF IndentationCheck
+ // Initial state
+ states.add(new StateInfo(mLinkDownState, null));
+ states.add(new StateInfo(mOfferExtendedState, null));
+ states.add(new StateInfo(mStoppedState, mOfferExtendedState));
+ states.add(new StateInfo(mStartedState, mOfferExtendedState));
+ states.add(new StateInfo(mRunningState, mStartedState));
+ // CHECKSTYLE:ON IndentationCheck
+ addAllStates(states);
+
+ // TODO: set initial state to TetheringState if a tethering interface has been requested and
+ // this is the first interface to be added.
+ start(mLinkDownState);
+ }
+
+ public boolean updateLinkState(boolean up) {
+ if (mLinkUp == up) {
+ return false;
+ }
+
+ // TODO: consider setting mLinkUp as part of processMessage().
+ mLinkUp = up;
+ if (!up) { // was up, goes down
+ processMessage(CMD_ON_LINK_DOWN);
+ } else { // was down, comes up
+ processMessage(CMD_ON_LINK_UP);
+ }
+
+ return true;
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 114cf2e..294a85a 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -51,12 +51,8 @@
import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TrafficStats.KB_IN_BYTES;
import static android.net.TrafficStats.MB_IN_BYTES;
-import static android.net.TrafficStats.TYPE_RX_BYTES;
-import static android.net.TrafficStats.TYPE_RX_PACKETS;
-import static android.net.TrafficStats.TYPE_TX_BYTES;
-import static android.net.TrafficStats.TYPE_TX_PACKETS;
import static android.net.TrafficStats.UID_TETHERING;
-import static android.net.TrafficStats.UNSUPPORTED;
+import static android.net.TrafficStats.getValueForTypeFromFirstEntry;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
@@ -74,12 +70,12 @@
import static com.android.net.module.util.DeviceConfigUtils.getDeviceConfigPropertyInt;
import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
-import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
@@ -242,13 +238,11 @@
// A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
// deadlock.
private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
-
/** Flags to control detail level of poll event. */
private static final int FLAG_PERSIST_NETWORK = 0x1;
private static final int FLAG_PERSIST_UID = 0x2;
private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
private static final int FLAG_PERSIST_FORCE = 0x100;
-
/**
* When global alert quota is high, wait for this delay before processing each polling,
* and do not schedule further polls once there is already one queued.
@@ -310,9 +304,16 @@
static final String TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME =
"trafficstats_cache_expiry_duration_ms";
- static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
+ static final String TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME =
+ "trafficstats_cache_max_entries";
static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
- static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+ static final int DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES = 400;
+ /**
+ * The delay time between to network stats update intents.
+ * Added to fix intent spams (b/343844995)
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ static final int BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS = 1000;
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -385,6 +386,7 @@
long getXtPersistBytes(long def);
long getUidPersistBytes(long def);
long getUidTagPersistBytes(long def);
+ long getBroadcastNetworkStatsUpdateDelayMs();
}
private final Object mStatsLock = new Object();
@@ -469,14 +471,33 @@
private long mLastStatsSessionPoll;
+ /**
+ * The timestamp of the most recent network stats broadcast.
+ *
+ * Note that this time could be in the past for completed broadcasts,
+ * or in the future for scheduled broadcasts.
+ *
+ * It is initialized to {@code Long.MIN_VALUE} to ensure that the first broadcast request
+ * is fulfilled immediately, regardless of the delay time.
+ *
+ * This value is used to enforce rate limiting on intents, preventing intent spam.
+ */
+ @GuardedBy("mStatsLock")
+ private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
+
private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
- static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
+ static final String TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG =
"trafficstats_rate_limit_cache_enabled_flag";
- private final boolean mAlwaysUseTrafficStatsRateLimitCache;
+ static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
+ "broadcast_network_stats_updated_rate_limit_enabled_flag";
+ private final boolean mAlwaysUseTrafficStatsServiceRateLimitCache;
private final int mTrafficStatsRateLimitCacheExpiryDuration;
- private final int mTrafficStatsRateLimitCacheMaxEntries;
+ private final int mTrafficStatsServiceRateLimitCacheMaxEntries;
+ private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
+
+
private final Object mOpenSessionCallsLock = new Object();
@@ -667,18 +688,23 @@
mEventLogger = null;
}
- mAlwaysUseTrafficStatsRateLimitCache =
- mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+ mAlwaysUseTrafficStatsServiceRateLimitCache =
+ mDeps.alwaysUseTrafficStatsServiceRateLimitCache(mContext);
+ mBroadcastNetworkStatsUpdatedRateLimitEnabled =
+ mDeps.enabledBroadcastNetworkStatsUpdatedRateLimiting(mContext);
mTrafficStatsRateLimitCacheExpiryDuration =
mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
- mTrafficStatsRateLimitCacheMaxEntries =
- mDeps.getTrafficStatsRateLimitCacheMaxEntries();
+ mTrafficStatsServiceRateLimitCacheMaxEntries =
+ mDeps.getTrafficStatsServiceRateLimitCacheMaxEntries();
mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
// TODO: Remove bpfNetMaps creation and always start SkDestroyListener
// Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -927,14 +953,25 @@
}
/**
- * Get whether TrafficStats rate-limit cache is always applied.
+ * Get whether broadcast network stats update rate limiting is enabled.
*
* This method should only be called once in the constructor,
* to ensure that the code does not need to deal with flag values changing at runtime.
*/
- public boolean alwaysUseTrafficStatsRateLimitCache(@NonNull Context ctx) {
+ public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ ctx, BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG);
+ }
+
+ /**
+ * Get whether TrafficStats service side rate-limit cache is always applied.
+ *
+ * This method should only be called once in the constructor,
+ * to ensure that the code does not need to deal with flag values changing at runtime.
+ */
+ public boolean alwaysUseTrafficStatsServiceRateLimitCache(@NonNull Context ctx) {
return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
- ctx, TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG);
+ ctx, TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG);
}
/**
@@ -950,15 +987,15 @@
}
/**
- * Get TrafficStats rate-limit cache max entries.
+ * Get TrafficStats service side rate-limit cache max entries.
*
* This method should only be called once in the constructor,
* to ensure that the code does not need to deal with flag values changing at runtime.
*/
- public int getTrafficStatsRateLimitCacheMaxEntries() {
+ public int getTrafficStatsServiceRateLimitCacheMaxEntries() {
return getDeviceConfigPropertyInt(
- NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
- DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES);
+ NAMESPACE_TETHERING, TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES);
}
/**
@@ -2098,20 +2135,28 @@
@Override
public long getUidStats(int uid, int type) {
+ return getValueForTypeFromFirstEntry(getTypelessUidStats(uid), type);
+ }
+
+ @NonNull
+ @Override
+ public NetworkStats getTypelessUidStats(int uid) {
+ final NetworkStats stats = new NetworkStats(0, 0);
final int callingUid = Binder.getCallingUid();
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
- return UNSUPPORTED;
+ return stats;
}
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
-
- if (mAlwaysUseTrafficStatsRateLimitCache
+ final NetworkStats.Entry entry;
+ if (mAlwaysUseTrafficStatsServiceRateLimitCache
|| mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
- final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+ entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
() -> mDeps.nativeGetUidStat(uid));
- return getEntryValueForType(entry, type);
- }
+ } else entry = mDeps.nativeGetUidStat(uid);
- return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+ if (entry != null) {
+ stats.insertEntry(entry);
+ }
+ return stats;
}
@Nullable
@@ -2128,50 +2173,24 @@
return entry;
}
+ @NonNull
@Override
- public long getIfaceStats(@NonNull String iface, int type) {
+ public NetworkStats getTypelessIfaceStats(@NonNull String iface) {
Objects.requireNonNull(iface);
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- if (mAlwaysUseTrafficStatsRateLimitCache
+ final NetworkStats.Entry entry;
+ if (mAlwaysUseTrafficStatsServiceRateLimitCache
|| mDeps.isChangeEnabled(
ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
- final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+ entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
() -> getIfaceStatsInternal(iface));
- return getEntryValueForType(entry, type);
- }
+ } else entry = getIfaceStatsInternal(iface);
- return getEntryValueForType(getIfaceStatsInternal(iface), type);
- }
-
- private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
- if (entry == null) return UNSUPPORTED;
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- switch (type) {
- case TYPE_RX_BYTES:
- return entry.rxBytes;
- case TYPE_RX_PACKETS:
- return entry.rxPackets;
- case TYPE_TX_BYTES:
- return entry.txBytes;
- case TYPE_TX_PACKETS:
- return entry.txPackets;
- default:
- throw new IllegalStateException("Bug: Invalid type: "
- + type + " should not reach here.");
+ NetworkStats stats = new NetworkStats(0, 0);
+ if (entry != null) {
+ stats.insertEntry(entry);
}
- }
-
- private boolean isEntryValueTypeValid(int type) {
- switch (type) {
- case TYPE_RX_BYTES:
- case TYPE_RX_PACKETS:
- case TYPE_TX_BYTES:
- case TYPE_TX_PACKETS:
- return true;
- default :
- return false;
- }
+ return stats;
}
@Nullable
@@ -2184,18 +2203,22 @@
return entry;
}
+ @NonNull
@Override
- public long getTotalStats(int type) {
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- if (mAlwaysUseTrafficStatsRateLimitCache
+ public NetworkStats getTypelessTotalStats() {
+ final NetworkStats.Entry entry;
+ if (mAlwaysUseTrafficStatsServiceRateLimitCache
|| mDeps.isChangeEnabled(
ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
- final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(
+ entry = mTrafficStatsTotalCache.getOrCompute(
IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
- return getEntryValueForType(entry, type);
- }
+ } else entry = getTotalStatsInternal();
- return getEntryValueForType(getTotalStatsInternal(), type);
+ final NetworkStats stats = new NetworkStats(0, 0);
+ if (entry != null) {
+ stats.insertEntry(entry);
+ }
+ return stats;
}
@Override
@@ -2645,8 +2668,22 @@
performSampleLocked();
}
- // finally, dispatch updated event to any listeners
- mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+ // Dispatch updated event to listeners, preventing intent spamming
+ // (b/343844995) possibly from abnormal modem RAT changes or misbehaving
+ // app calls (see NetworkStatsEventLogger#POLL_REASON_* for possible reasons).
+ // If no broadcasts are scheduled, use the time of the last broadcast
+ // to schedule the next one ASAP.
+ if (!mBroadcastNetworkStatsUpdatedRateLimitEnabled) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+ } else if (mLatestNetworkStatsUpdatedBroadcastScheduledTime < SystemClock.uptimeMillis()) {
+ mLatestNetworkStatsUpdatedBroadcastScheduledTime = Math.max(
+ mLatestNetworkStatsUpdatedBroadcastScheduledTime
+ + mSettings.getBroadcastNetworkStatsUpdateDelayMs(),
+ SystemClock.uptimeMillis()
+ );
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED),
+ mLatestNetworkStatsUpdatedBroadcastScheduledTime);
+ }
Trace.traceEnd(TRACE_TAG_NETWORK);
}
@@ -2959,12 +2996,14 @@
} catch (IOException e) {
pw.println("(failed to dump FastDataInput counters)");
}
- pw.print("trafficstats.cache.alwaysuse", mAlwaysUseTrafficStatsRateLimitCache);
+ pw.print("trafficstats.service.cache.alwaysuse",
+ mAlwaysUseTrafficStatsServiceRateLimitCache);
pw.println();
pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
mTrafficStatsRateLimitCacheExpiryDuration);
pw.println();
- pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME, mTrafficStatsRateLimitCacheMaxEntries);
+ pw.print(TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
pw.println();
pw.decreaseIndent();
@@ -3605,6 +3644,11 @@
public long getUidTagPersistBytes(long def) {
return def;
}
+
+ @Override
+ public long getBroadcastNetworkStatsUpdateDelayMs() {
+ return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+ }
}
// TODO: Read stats by using BpfNetMapsReader.
diff --git a/service/Android.bp b/service/Android.bp
index 1dd09a9..94061a4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -161,7 +161,7 @@
],
libs: [
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
// so any calls to it must be protected with a check to ensure that it is
@@ -175,12 +175,12 @@
// TODO: figure out why just using "framework-tethering" uses the stubs, even though both
// service-connectivity and framework-tethering are in the same APEX.
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"unsupportedappusage",
"ServiceConnectivityResources",
- "framework-statsd",
- "framework-permission",
- "framework-permission-s",
+ "framework-statsd.stubs.module_lib",
+ "framework-permission.stubs.module_lib",
+ "framework-permission-s.stubs.module_lib",
],
static_libs: [
// Do not add libs here if they are already included
@@ -190,18 +190,12 @@
"connectivity_native_aidl_interface-lateststable-java",
"dnsresolver_aidl_interface-V15-java",
"modules-utils-shell-command-handler",
- "net-utils-device-common",
- "net-utils-device-common-ip",
- "net-utils-device-common-netlink",
- "net-utils-services-common",
+ "net-utils-service-connectivity",
"netd-client",
"networkstack-client",
"PlatformProperties",
"service-connectivity-protos",
"service-connectivity-stats-protos",
- // The required dependency net-utils-device-common-struct-base is in the classpath via
- // framework-connectivity
- "net-utils-device-common-struct",
],
apex_available: [
"com.android.tethering",
@@ -212,6 +206,7 @@
},
visibility: [
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/networksecurity:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/service:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
@@ -253,6 +248,7 @@
"service-connectivity-pre-jarjar",
"service-connectivity-tiramisu-pre-jarjar",
"service-nearby-pre-jarjar",
+ "service-networksecurity-pre-jarjar",
service_remoteauth_pre_jarjar_lib,
"service-thread-pre-jarjar",
],
@@ -268,10 +264,10 @@
"framework-connectivity.impl",
"framework-connectivity-t.impl",
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"libprotobuf-java-nano",
- "framework-permission",
- "framework-permission-s",
+ "framework-permission.stubs.module_lib",
+ "framework-permission-s.stubs.module_lib",
],
jarjar_rules: ":connectivity-jarjar-rules",
apex_available: [
@@ -321,6 +317,7 @@
":framework-connectivity-jarjar-rules",
":service-connectivity-jarjar-gen",
":service-nearby-jarjar-gen",
+ ":service-networksecurity-jarjar-gen",
":service-remoteauth-jarjar-gen",
":service-thread-jarjar-gen",
],
@@ -410,6 +407,24 @@
visibility: ["//visibility:private"],
}
+java_genrule {
+ name: "service-networksecurity-jarjar-gen",
+ tool_files: [
+ ":service-networksecurity-pre-jarjar{.jar}",
+ "jarjar-excludes.txt",
+ ],
+ tools: [
+ "jarjar-rules-generator",
+ ],
+ out: ["service_ct_jarjar_rules.txt"],
+ cmd: "$(location jarjar-rules-generator) " +
+ "$(location :service-networksecurity-pre-jarjar{.jar}) " +
+ "--prefix com.android.server.net.ct " +
+ "--excludes $(location jarjar-excludes.txt) " +
+ "--output $(out)",
+ visibility: ["//visibility:private"],
+}
+
genrule {
name: "statslog-connectivity-java-gen",
tools: ["stats-log-api-gen"],
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2621256..be9b2b5 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -33,6 +33,7 @@
"com.android.tethering",
],
certificate: ":com.android.connectivity.resources.certificate",
+ updatable: true,
}
android_app_certificate {
diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml
index 92dd9a1..8cefec4 100644
--- a/service/ServiceConnectivityResources/res/values-ar/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml
@@ -40,7 +40,7 @@
<item msgid="5624324321165953608">"Wi-Fi"</item>
<item msgid="5667906231066981731">"بلوتوث"</item>
<item msgid="346574747471703768">"إيثرنت"</item>
- <item msgid="5734728378097476003">"شبكة افتراضية خاصة (VPN)"</item>
+ <item msgid="5734728378097476003">"شبكة VPN"</item>
</string-array>
<string name="network_switch_type_name_unknown" msgid="5116448402191972082">"نوع شبكة غير معروف"</string>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index 81d8ddb..5a0a9d4 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistemaren konexio-baliabideak"</string>
- <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa Wi-Fi sarean"</string>
+ <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa wifi-sarean"</string>
<string name="network_available_sign_in" msgid="2622520134876355561">"Hasi saioa sarean"</string>
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 02a9ce6..4027038 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -54,9 +54,21 @@
-->
<string translatable="false" name="config_thread_model_name">Thread Border Router</string>
- <!-- Whether the Thread network will be managed by the Google Home ecosystem. When this value
- is set, a TXT entry "vgh=0" or "vgh=1" will be added to the "_mehscop._udp" mDNS service
- respectively (The TXT value is a string).
+ <!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
+ service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For
+ example, the key and value of each TXT entry MUST be separated with "=". If the value length is
+ 0, the trailing "=" may be omitted. Additionally, the TXT keys MUST start with "v" and be at
+ least 2 characters.
+
+ Note, do not include credentials in any of the TXT entries - they will be advertised on Wi-Fi
+ or Ethernet link.
+
+ An example config can be:
+ <string-array name="config_thread_mdns_vendor_specific_txts">
+ <item>vab=123</item>
+ <item>vcd</item>
+ </string-array>
-->
- <bool name="config_thread_managed_by_google_home">false</bool>
+ <string-array name="config_thread_mdns_vendor_specific_txts">
+ </string-array>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 158b0c8..fbaae05 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -51,6 +51,7 @@
<item type="string" name="config_thread_vendor_name" />
<item type="string" name="config_thread_vendor_oui" />
<item type="string" name="config_thread_model_name" />
+ <item type="array" name="config_thread_mdns_vendor_specific_txts" />
</policy>
</overlayable>
</resources>
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 3e11d52..b09589c 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -3,17 +3,6 @@
<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=" ~~~~~~~~~~~~~">
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index b3e7d8c..44868b2d 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -580,6 +580,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public int getUidRule(final int childChain, final int uid) {
return BpfNetMapsUtils.getUidRule(sUidOwnerMap, childChain, uid);
}
diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java
new file mode 100644
index 0000000..4e068ea
--- /dev/null
+++ b/service/src/com/android/server/CallbackQueue.java
@@ -0,0 +1,125 @@
+/*
+ * 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.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+
+import com.android.net.module.util.GrowingIntArray;
+
+/**
+ * A utility class to add/remove {@link NetworkCallback}s from a queue.
+ *
+ * <p>This is intended to be used as a temporary builder to create/modify callbacks stored in an int
+ * array for memory efficiency.
+ *
+ * <p>Intended usage:
+ * <pre>
+ * final CallbackQueue queue = new CallbackQueue(storedCallbacks);
+ * queue.forEach(netId, callbackId -> { [...] });
+ * queue.addCallback(netId, callbackId);
+ * [...]
+ * storedCallbacks = queue.getMinimizedBackingArray();
+ * </pre>
+ *
+ * <p>This class is not thread-safe.
+ */
+public class CallbackQueue extends GrowingIntArray {
+ public CallbackQueue(int[] initialCallbacks) {
+ super(initialCallbacks);
+ }
+
+ /**
+ * Get a callback int from netId and callbackId.
+ *
+ * <p>The first 16 bits of each int is the netId; the last 16 bits are the callback index.
+ */
+ private static int getCallbackInt(int netId, int callbackId) {
+ return (netId << 16) | (callbackId & 0xffff);
+ }
+
+ private static int getNetId(int callbackInt) {
+ return callbackInt >>> 16;
+ }
+
+ private static int getCallbackId(int callbackInt) {
+ return callbackInt & 0xffff;
+ }
+
+ /**
+ * A consumer interface for {@link #forEach(CallbackConsumer)}.
+ *
+ * <p>This is similar to a BiConsumer<Integer, Integer>, but avoids the boxing cost.
+ */
+ public interface CallbackConsumer {
+ /**
+ * Method called on each callback in the queue.
+ */
+ void accept(int netId, int callbackId);
+ }
+
+ /**
+ * Iterate over all callbacks in the queue.
+ */
+ public void forEach(@NonNull CallbackConsumer consumer) {
+ forEach(value -> {
+ final int netId = getNetId(value);
+ final int callbackId = getCallbackId(value);
+ consumer.accept(netId, callbackId);
+ });
+ }
+
+ /**
+ * Indicates whether the queue contains a callback for the given (netId, callbackId).
+ */
+ public boolean hasCallback(int netId, int callbackId) {
+ return contains(getCallbackInt(netId, callbackId));
+ }
+
+ /**
+ * Remove all callbacks for the given netId.
+ *
+ * @return true if at least one callback was removed.
+ */
+ public boolean removeCallbacksForNetId(int netId) {
+ return removeValues(cb -> getNetId(cb) == netId);
+ }
+
+ /**
+ * Remove all callbacks for the given netId and callbackId.
+ * @return true if at least one callback was removed.
+ */
+ public boolean removeCallbacks(int netId, int callbackId) {
+ final int cbInt = getCallbackInt(netId, callbackId);
+ return removeValues(cb -> cb == cbInt);
+ }
+
+ /**
+ * Add a callback at the end of the queue.
+ */
+ public void addCallback(int netId, int callbackId) {
+ add(getCallbackInt(netId, callbackId));
+ }
+
+ @Override
+ protected String valueToString(int item) {
+ final int callbackId = getCallbackId(item);
+ final int netId = getNetId(item);
+ return ConnectivityManager.getCallbackName(callbackId) + "(" + netId + ")";
+ }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 5186203..cb62ae1 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,14 +38,27 @@
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.CALLBACK_AVAILABLE;
+import static android.net.ConnectivityManager.CALLBACK_BLK_CHANGED;
+import static android.net.ConnectivityManager.CALLBACK_CAP_CHANGED;
import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
+import static android.net.ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED;
+import static android.net.ConnectivityManager.CALLBACK_LOSING;
+import static android.net.ConnectivityManager.CALLBACK_LOST;
+import static android.net.ConnectivityManager.CALLBACK_PRECHECK;
+import static android.net.ConnectivityManager.CALLBACK_RESUMED;
+import static android.net.ConnectivityManager.CALLBACK_SUSPENDED;
+import static android.net.ConnectivityManager.CALLBACK_UNAVAIL;
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;
+import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
@@ -109,7 +122,6 @@
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
import static android.system.OsConstants.ETH_P_ALL;
-import static android.system.OsConstants.F_OK;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -134,10 +146,9 @@
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
-import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
-
-import static java.util.Map.Entry;
+import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
import android.Manifest;
import android.annotation.CheckResult;
@@ -279,7 +290,6 @@
import android.stats.connectivity.ValidatedState;
import android.sysprop.NetworkProperties;
import android.system.ErrnoException;
-import android.system.Os;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -323,6 +333,7 @@
import com.android.net.module.util.LocationPermissionChecker;
import com.android.net.module.util.PerUidCounter;
import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.RoutingCoordinatorService;
import com.android.net.module.util.TcUtils;
import com.android.net.module.util.netlink.InetDiagMessage;
import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
@@ -360,7 +371,6 @@
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;
@@ -393,11 +403,11 @@
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
-import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@@ -503,6 +513,9 @@
private final boolean mUseDeclaredMethodsForCallbacksEnabled;
+ // Flag to delay callbacks for frozen apps, suppressing duplicate and stale callbacks.
+ private final boolean mQueueCallbacksForFrozenApps;
+
/**
* Uids ConnectivityService tracks blocked status of to send blocked status callbacks.
* Key is uid based on mAsUid of registered networkRequestInfo
@@ -1791,7 +1804,7 @@
mDefaultRequest = new NetworkRequestInfo(
Process.myUid(), defaultInternetRequest, null,
null /* binder */, NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
- null /* attributionTags */);
+ null /* attributionTags */, DECLARED_METHODS_NONE);
mNetworkRequests.put(defaultInternetRequest, mDefaultRequest);
mDefaultNetworkRequests.add(mDefaultRequest);
mNetworkRequestInfoLogs.log("REGISTER " + mDefaultRequest);
@@ -1845,35 +1858,6 @@
new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
mHandlerThread.start();
mHandler = new InternalHandler(mHandlerThread.getLooper());
- // Temporary hack to report netbpfload result.
- // TODO: remove in 2024-09 when netbpfload starts loading mainline bpf programs.
- if (!mDeps.isAtLeastV()) {
- mHandler.postDelayed(() -> {
- // Test Log.wtf reporting pipeline. Ignore this Log.wtf if it shows up in the logs.
- final Random r = new Random();
- if (Build.TYPE.equals("user") && r.nextInt(1000) == 0) {
- Log.wtf(TAG, "NOT A FAILURE, PLEASE IGNORE! Ensure netbpfload result reported");
- }
- // Did netbpfload create the map?
- try {
- Os.access("/sys/fs/bpf/net_shared/map_gentle_test", F_OK);
- } catch (ErrnoException e) {
- Log.wtf(TAG, "netbpfload did not create map", e);
- }
- // Did netbpfload create the program?
- try {
- Os.access("/sys/fs/bpf/net_shared/prog_gentle_skfilter_accept", F_OK);
- } catch (ErrnoException e) {
- Log.wtf(TAG, "netbpfload did not create program", e);
- }
- // Did netbpfload run to completion?
- try {
- Os.access("/sys/fs/bpf/netd_shared/mainline_done", F_OK);
- } catch (ErrnoException e) {
- Log.wtf(TAG, "netbpfload did not run to completion", e);
- }
- }, 30_000 /* delayMillis */);
- }
mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
mConnectivityDiagnosticsHandler =
new ConnectivityDiagnosticsHandler(mHandlerThread.getLooper());
@@ -1895,6 +1879,9 @@
context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+ // registerUidFrozenStateChangedCallback is only available on U+
+ mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
+ && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
this::handleUidCarrierPrivilegesLost, mHandler);
@@ -2050,7 +2037,7 @@
mDelayDestroySockets = mDeps.isFeatureNotChickenedOut(context, DELAY_DESTROY_SOCKETS);
mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
- if (mDestroyFrozenSockets) {
+ if (mDestroyFrozenSockets || mQueueCallbacksForFrozenApps) {
final UidFrozenStateChangedCallback frozenStateChangedCallback =
new UidFrozenStateChangedCallback() {
@Override
@@ -2162,7 +2149,7 @@
}
@VisibleForTesting
- void updateMobileDataPreferredUids() {
+ public void updateMobileDataPreferredUids() {
mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
}
@@ -2207,7 +2194,7 @@
handleRegisterNetworkRequest(new NetworkRequestInfo(
Process.myUid(), networkRequest, null /* messenger */, null /* binder */,
NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
- null /* attributionTags */));
+ null /* attributionTags */, DECLARED_METHODS_NONE));
} else {
handleReleaseNetworkRequest(networkRequest, Process.SYSTEM_UID,
/* callOnUnavailable */ false);
@@ -3418,7 +3405,7 @@
}
@VisibleForTesting
- void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
+ public void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
for (Pair<Integer, Integer> reasons: reasonsList) {
final int uid = reasons.first;
final int blockedReasons = reasons.second;
@@ -3486,6 +3473,14 @@
private void handleFrozenUids(int[] uids, int[] frozenStates) {
ensureRunningOnConnectivityServiceThread();
+ handleDestroyFrozenSockets(uids, frozenStates);
+ handleFreezeNetworkCallbacks(uids, frozenStates);
+ }
+
+ private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) {
+ if (!mDestroyFrozenSockets) {
+ return;
+ }
for (int i = 0; i < uids.length; i++) {
final int uid = uids[i];
final boolean addReason = frozenStates[i] == UID_FROZEN_STATE_FROZEN;
@@ -3497,6 +3492,73 @@
}
}
+ private void handleFreezeNetworkCallbacks(int[] uids, int[] frozenStates) {
+ if (!mQueueCallbacksForFrozenApps) {
+ return;
+ }
+ for (int i = 0; i < uids.length; i++) {
+ final int uid = uids[i];
+ // These counters may be modified on different threads, but using them here is fine
+ // because this is only an optimization where wrong behavior would only happen if they
+ // are zero even though there is a request registered. This is not possible as they are
+ // always incremented before posting messages to register, and decremented on the
+ // handler thread when unregistering.
+ if (mSystemNetworkRequestCounter.get(uid) == 0
+ && mNetworkRequestCounter.get(uid) == 0) {
+ // Avoid iterating requests if there isn't any. The counters only track app requests
+ // and not internal requests (for example always-on requests which do not have a
+ // mMessenger), so it does not completely match the content of mRequests. This is OK
+ // as only app requests need to be frozen.
+ continue;
+ }
+
+ if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
+ freezeNetworkCallbacksForUid(uid);
+ } else {
+ unfreezeNetworkCallbacksForUid(uid);
+ }
+ }
+ }
+
+ /**
+ * Suspend callbacks for a UID that was just frozen.
+ *
+ * <p>Note that it is not possible for a process to be frozen during a blocking binder call
+ * (see CachedAppOptimizer.freezeBinder), and IConnectivityManager callback registrations are
+ * blocking binder calls, so no callback can be registered while the UID is frozen. This means
+ * it is not necessary to check frozen state on new callback registrations, and calling this
+ * method when a UID is newly frozen is sufficient.
+ *
+ * <p>If it ever becomes possible for a process to be frozen during a blocking binder call,
+ * ConnectivityService will need to handle freezing callbacks that reach ConnectivityService
+ * after the app was frozen when being registered.
+ */
+ private void freezeNetworkCallbacksForUid(int uid) {
+ if (DDBG) Log.d(TAG, "Freezing callbacks for UID " + uid);
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.mUid != uid) continue;
+ // mNetworkRequests can have duplicate values for multilayer requests, but calling
+ // onFrozen multiple times is fine.
+ // If freezeNetworkCallbacksForUid was called multiple times in a raw for a frozen UID
+ // (which would be incorrect), this would also handle it gracefully.
+ nri.onFrozen();
+ }
+ }
+
+ private void unfreezeNetworkCallbacksForUid(int uid) {
+ // This sends all callbacks for one NetworkRequest at a time, which may not be the
+ // same order they were queued in, but different network requests use different
+ // binder objects, so the relative order of their callbacks is not guaranteed.
+ // If callbacks are not queued, callbacks from different binder objects may be
+ // posted on different threads when the process is unfrozen, so even if they were
+ // called a long time apart while the process was frozen, they may still appear in
+ // different order when unfreezing it.
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.mUid != uid) continue;
+ nri.sendQueuedCallbacks();
+ }
+ }
+
private void handleUpdateFirewallDestroySocketReasons(
List<Pair<Integer, Integer>> reasonsList) {
if (!shouldTrackFirewallDestroySocketReasons()) {
@@ -4378,9 +4440,8 @@
pw.println();
pw.println("Multicast routing supported: " +
(mMulticastRoutingCoordinatorService != null));
-
- pw.println();
pw.println("Background firewall chain enabled: " + mBackgroundFirewallChainEnabled);
+ pw.println("IngressToVpnAddressFiltering: " + mIngressToVpnAddressFiltering);
}
private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5396,7 +5457,7 @@
// by other networks that are already connected. Perhaps that can be done by
// sending all CALLBACK_LOST messages (for requests, not listens) at the end
// of rematchAllNetworksAndRequests
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
+ notifyNetworkCallbacks(nai, CALLBACK_LOST);
mKeepaliveTracker.handleStopAllKeepalives(nai, SocketKeepalive.ERROR_INVALID_NETWORK);
mQosCallbackTracker.handleNetworkReleased(nai.network);
@@ -5518,8 +5579,7 @@
// correctly contains null as an upstream.
if (sendCallbacks) {
nri.setSatisfier(null, null);
- notifyNetworkCallbacks(local,
- ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ notifyNetworkCallbacks(local, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
}
}
@@ -5894,8 +5954,7 @@
log("releasing " + nri.mRequests.get(0) + " (timeout)");
}
handleRemoveNetworkRequest(nri);
- callCallbackForRequest(
- nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+ callCallbackForRequest(nri, null, CALLBACK_UNAVAIL, 0);
}
private void handleReleaseNetworkRequest(@NonNull final NetworkRequest request,
@@ -5911,7 +5970,7 @@
}
handleRemoveNetworkRequest(nri);
if (callOnUnavailable) {
- callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+ callCallbackForRequest(nri, null, CALLBACK_UNAVAIL, 0);
}
}
@@ -5943,7 +6002,7 @@
// TODO : The only way out of this is to diff old defaults and new defaults, and only
// remove ranges for those requests that won't have a replacement
final NetworkAgentInfo satisfier = nri.getSatisfier();
- if (null != satisfier) {
+ if (null != satisfier && !satisfier.isDestroyed()) {
try {
mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
satisfier.network.getNetId(),
@@ -7067,7 +7126,7 @@
// should have its link properties fixed up for PAC proxies.
mProxyTracker.updateDefaultNetworkProxyPortForPAC(nai.linkProperties, nai.network);
if (nai.everConnected()) {
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_IP_CHANGED);
+ notifyNetworkCallbacks(nai, CALLBACK_IP_CHANGED);
}
}
@@ -7553,6 +7612,29 @@
// single NetworkRequest in mRequests.
final List<NetworkRequest> mRequests;
+ /**
+ * List of callbacks that are queued for sending later when the requesting app is unfrozen.
+ *
+ * <p>There may typically be hundreds of NetworkRequestInfo, so a memory-efficient structure
+ * (just an int[]) is used to keep queued callbacks. This reduces the number of object
+ * references.
+ *
+ * <p>This is intended to be used with {@link CallbackQueue} which defines the internal
+ * format.
+ */
+ @NonNull
+ private int[] mQueuedCallbacks = new int[0];
+
+ private static final int MATCHED_NETID_NOT_FROZEN = -1;
+
+ /**
+ * If this request was already satisfied by a network when the requesting UID was frozen,
+ * the netId that was matched at that time. Otherwise, NETID_UNSET if no network was
+ * satisfying this request when frozen (including if this is a listen and not a request),
+ * and MATCHED_NETID_NOT_FROZEN if not frozen.
+ */
+ private int mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+
// mSatisfier and mActiveRequest rely on one another therefore set them together.
void setSatisfier(
@Nullable final NetworkAgentInfo satisfier,
@@ -7609,6 +7691,8 @@
// Preference order of this request.
final int mPreferenceOrder;
+ final int mDeclaredMethodsFlags;
+
// In order to preserve the mapping of NetworkRequest-to-callback when apps register
// callbacks using a returned NetworkRequest, the original NetworkRequest needs to be
// maintained for keying off of. This is only a concern when the original nri
@@ -7662,21 +7746,22 @@
mCallbackFlags = NetworkCallback.FLAG_NONE;
mCallingAttributionTag = callingAttributionTag;
mPreferenceOrder = preferenceOrder;
+ mDeclaredMethodsFlags = DECLARED_METHODS_NONE;
}
NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m,
@Nullable final IBinder binder,
@NetworkCallback.Flag int callbackFlags,
- @Nullable String callingAttributionTag) {
+ @Nullable String callingAttributionTag, int declaredMethodsFlags) {
this(asUid, Collections.singletonList(r), r, m, binder, callbackFlags,
- callingAttributionTag);
+ callingAttributionTag, declaredMethodsFlags);
}
NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
@NonNull final NetworkRequest requestForCallback, @Nullable final Messenger m,
@Nullable final IBinder binder,
@NetworkCallback.Flag int callbackFlags,
- @Nullable String callingAttributionTag) {
+ @Nullable String callingAttributionTag, int declaredMethodsFlags) {
super();
ensureAllNetworkRequestsHaveType(r);
mRequests = initializeRequests(r);
@@ -7691,6 +7776,7 @@
mCallbackFlags = callbackFlags;
mCallingAttributionTag = callingAttributionTag;
mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+ mDeclaredMethodsFlags = declaredMethodsFlags;
linkDeathRecipient();
}
@@ -7720,6 +7806,8 @@
}
setSatisfier(satisfier, activeRequest);
}
+ mMatchedNetIdWhenFrozen = nri.mMatchedNetIdWhenFrozen;
+ mQueuedCallbacks = nri.mQueuedCallbacks;
mMessenger = nri.mMessenger;
mBinder = nri.mBinder;
mPid = nri.mPid;
@@ -7731,6 +7819,7 @@
mCallingAttributionTag = nri.mCallingAttributionTag;
mUidTrackedForBlockedStatus = nri.mUidTrackedForBlockedStatus;
mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+ mDeclaredMethodsFlags = nri.mDeclaredMethodsFlags;
linkDeathRecipient();
}
@@ -7783,6 +7872,190 @@
}
}
+ /**
+ * Called when this NRI is being frozen.
+ *
+ * <p>Calling this method multiple times when the NRI is frozen is fine. This may happen
+ * if iterating through the NetworkRequest -> NRI map since there are duplicates in the
+ * NRI values for multilayer requests. It may also happen if an app is frozen, killed,
+ * restarted and refrozen since there is no callback sent when processes are killed, but in
+ * that case the callbacks to the killed app do not matter.
+ */
+ void onFrozen() {
+ if (mMatchedNetIdWhenFrozen != MATCHED_NETID_NOT_FROZEN) {
+ // Already frozen
+ return;
+ }
+ if (mSatisfier != null) {
+ mMatchedNetIdWhenFrozen = mSatisfier.network.netId;
+ } else {
+ mMatchedNetIdWhenFrozen = NETID_UNSET;
+ }
+ }
+
+ boolean maybeQueueCallback(@NonNull NetworkAgentInfo nai, int callbackId) {
+ if (mMatchedNetIdWhenFrozen == MATCHED_NETID_NOT_FROZEN) {
+ return false;
+ }
+
+ boolean ignoreThisCallback = false;
+ final int netId = nai.network.netId;
+ final CallbackQueue queue = new CallbackQueue(mQueuedCallbacks);
+ // Based on the new callback, clear previous callbacks that are no longer necessary.
+ // For example, if the network is lost, there is no need to send intermediate callbacks.
+ switch (callbackId) {
+ // PRECHECK is not an API and not very meaningful, do not deliver it for frozen apps
+ // Networks are likely to already be lost when the app is unfrozen, also skip LOSING
+ case CALLBACK_PRECHECK:
+ case CALLBACK_LOSING:
+ ignoreThisCallback = true;
+ break;
+ case CALLBACK_LOST:
+ // All callbacks for this netId before onLost are unnecessary. And onLost itself
+ // is also unnecessary if onAvailable was previously queued for this netId: the
+ // Network just appeared and disappeared while the app was frozen.
+ ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+ queue.removeCallbacksForNetId(netId);
+ break;
+ case CALLBACK_AVAILABLE:
+ if (mSatisfier != null) {
+ // For requests that are satisfied by individual networks (not LISTEN), when
+ // AVAILABLE is received, the request is matching a new Network, so previous
+ // callbacks (for other Networks) are unnecessary.
+ queue.clear();
+ }
+ break;
+ case CALLBACK_SUSPENDED:
+ case CALLBACK_RESUMED:
+ if (queue.hasCallback(netId, CALLBACK_AVAILABLE)) {
+ // AVAILABLE will already send the latest suspended status
+ ignoreThisCallback = true;
+ break;
+ }
+ // If SUSPENDED was queued, just remove it from the queue instead of sending
+ // RESUMED; and vice-versa.
+ final int otherCb = callbackId == CALLBACK_SUSPENDED
+ ? CALLBACK_RESUMED
+ : CALLBACK_SUSPENDED;
+ ignoreThisCallback = queue.removeCallbacks(netId, otherCb);
+ break;
+ case CALLBACK_CAP_CHANGED:
+ case CALLBACK_IP_CHANGED:
+ case CALLBACK_LOCAL_NETWORK_INFO_CHANGED:
+ case CALLBACK_BLK_CHANGED:
+ ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+ break;
+ default:
+ Log.wtf(TAG, "Unexpected callback type: "
+ + ConnectivityManager.getCallbackName(callbackId));
+ return false;
+ }
+
+ if (!ignoreThisCallback) {
+ // For non-listen (matching) callbacks, AVAILABLE can appear in the queue twice in a
+ // row for the same network if the new AVAILABLE suppressed intermediate AVAILABLEs
+ // for other networks. Example:
+ // A is matched, app is frozen, B is matched, A is matched again (removes callbacks
+ // for B), app is unfrozen.
+ // In that case call AVAILABLE sub-callbacks to update state, but not AVAILABLE
+ // itself.
+ if (callbackId == CALLBACK_AVAILABLE && netId == mMatchedNetIdWhenFrozen) {
+ // The queue should have been cleared here, since this is AVAILABLE on a
+ // non-listen callback (mMatchedNetIdWhenFrozen is set).
+ addAvailableSubCallbacks(nai, queue);
+ } else {
+ // When unfreezing, no need to send a callback multiple times for the same netId
+ queue.removeCallbacks(netId, callbackId);
+ // TODO: this code always adds the callback for simplicity. It would save
+ // some CPU/memory if the code instead only added to the queue callbacks where
+ // isCallbackOverridden=true, or which need to be in the queue because they
+ // affect other callbacks that are overridden.
+ queue.addCallback(netId, callbackId);
+ }
+ }
+ // Instead of shrinking the queue, possibly reallocating, the NRI could keep the array
+ // and length in memory for future adds, but this saves memory by avoiding the cost
+ // of an extra member and of unused array length (there are often hundreds of NRIs).
+ mQueuedCallbacks = queue.getMinimizedBackingArray();
+ return true;
+ }
+
+ /**
+ * Called when this NRI is being unfrozen to stop queueing, and send queued callbacks.
+ *
+ * <p>Calling this method multiple times when the NRI is unfrozen (for example iterating
+ * through the NetworkRequest -> NRI map where there are duplicate values for multilayer
+ * requests) is fine.
+ */
+ void sendQueuedCallbacks() {
+ mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+ if (mQueuedCallbacks.length == 0) {
+ return;
+ }
+ new CallbackQueue(mQueuedCallbacks).forEach((netId, callbackId) -> {
+ // For CALLBACK_LOST only, there will not be a NAI for the netId. Build and send the
+ // callback directly.
+ if (callbackId == CALLBACK_LOST) {
+ if (isCallbackOverridden(CALLBACK_LOST)) {
+ final Bundle cbBundle = makeCommonBundleForCallback(this,
+ new Network(netId));
+ callCallbackForRequest(this, CALLBACK_LOST, cbBundle, 0 /* arg1 */);
+ }
+ return; // Next item in forEach
+ }
+
+ // Other callbacks should always have a NAI, because if a Network disconnects
+ // LOST will be called, unless the request is no longer satisfied by that Network in
+ // which case AVAILABLE will have been called for another Network. In both cases
+ // previous callbacks are cleared.
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+ if (nai == null) {
+ Log.wtf(TAG, "Missing NetworkAgentInfo for net " + netId
+ + " for callback " + callbackId);
+ return; // Next item in forEach
+ }
+
+ final int arg1 =
+ callbackId == CALLBACK_AVAILABLE || callbackId == CALLBACK_BLK_CHANGED
+ ? getBlockedState(nai, mAsUid)
+ : 0;
+ callCallbackForRequest(this, nai, callbackId, arg1);
+ });
+ mQueuedCallbacks = new int[0];
+ }
+
+ boolean isCallbackOverridden(int callbackId) {
+ return !mUseDeclaredMethodsForCallbacksEnabled
+ || (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
+ }
+
+ /**
+ * Queue all callbacks that are called by AVAILABLE, except onAvailable.
+ *
+ * <p>AVAILABLE may call SUSPENDED, CAP_CHANGED, IP_CHANGED, LOCAL_NETWORK_INFO_CHANGED,
+ * and BLK_CHANGED, in this order.
+ */
+ private void addAvailableSubCallbacks(
+ @NonNull NetworkAgentInfo nai, @NonNull CallbackQueue queue) {
+ final boolean callSuspended =
+ !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ final boolean callLocalInfoChanged = nai.isLocalNetwork();
+
+ final int cbCount = 3 + (callSuspended ? 1 : 0) + (callLocalInfoChanged ? 1 : 0);
+ // Avoid unnecessary re-allocations by reserving enough space for all callbacks to add.
+ queue.ensureHasCapacity(cbCount);
+ final int netId = nai.network.netId;
+ if (callSuspended) {
+ queue.addCallback(netId, CALLBACK_SUSPENDED);
+ }
+ queue.addCallback(netId, CALLBACK_CAP_CHANGED);
+ queue.addCallback(netId, CALLBACK_IP_CHANGED);
+ if (callLocalInfoChanged) {
+ queue.addCallback(netId, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ }
+ queue.addCallback(netId, CALLBACK_BLK_CHANGED);
+ }
+
boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
// Compare two preference orders.
return mPreferenceOrder < target.mPreferenceOrder;
@@ -7818,10 +8091,53 @@
+ (mPendingIntent == null ? "" : " to trigger " + mPendingIntent)
+ " callback flags: " + mCallbackFlags
+ " order: " + mPreferenceOrder
- + " isUidTracked: " + mUidTrackedForBlockedStatus;
+ + " isUidTracked: " + mUidTrackedForBlockedStatus
+ + " declaredMethods: " + declaredMethodsFlagsToString(mDeclaredMethodsFlags);
}
}
+ /**
+ * Get a readable String for a bitmask of declared methods.
+ */
+ @VisibleForTesting
+ public static String declaredMethodsFlagsToString(int flags) {
+ if (flags == DECLARED_METHODS_NONE) {
+ return "NONE";
+ }
+ if (flags == DECLARED_METHODS_ALL) {
+ return "ALL";
+ }
+ final StringBuilder sb = new StringBuilder();
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_PRECHECK, "PRECHK", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_AVAILABLE, "AVAIL", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOSING, "LOSING", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOST, "LOST", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_UNAVAIL, "UNAVAIL", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_CAP_CHANGED, "NC", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_IP_CHANGED, "LP", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_SUSPENDED, "SUSP", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_RESUMED, "RESUME", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_BLK_CHANGED, "BLK", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOCAL_NETWORK_INFO_CHANGED,
+ "LOCALINF", sb);
+ if (flags != 0) {
+ sb.append("|0x").append(Integer.toHexString(flags));
+ }
+ return sb.toString();
+ }
+
+ private static int maybeAppendDeclaredMethod(int declaredMethodsFlags,
+ int callbackId, String callbackName, @NonNull StringBuilder builder) {
+ final int callbackFlag = 1 << callbackId;
+ if ((declaredMethodsFlags & callbackFlag) != 0) {
+ if (builder.length() > 0) {
+ builder.append('|');
+ }
+ builder.append(callbackName);
+ }
+ return declaredMethodsFlags & ~callbackFlag;
+ }
+
// Keep backward compatibility since the ServiceSpecificException is used by
// the API surface, see {@link ConnectivityManager#convertServiceException}.
public static class RequestInfoPerUidCounter extends PerUidCounter {
@@ -7956,7 +8272,21 @@
public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities,
int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
int legacyType, int callbackFlags, @NonNull String callingPackageName,
- @Nullable String callingAttributionTag) {
+ @Nullable String callingAttributionTag, int declaredMethodsFlag) {
+ if (declaredMethodsFlag == 0) {
+ // This could happen if raw binder calls are used to call the previous overload of
+ // requestNetwork, as missing int arguments in a binder call end up as 0
+ // (Parcel.readInt returns 0 at the end of a parcel). Such raw calls this would be
+ // really unexpected bad behavior from the caller though.
+ // TODO: remove after verifying this does not happen. This could allow enabling the
+ // optimization for callbacks that do not override any method (right now they use
+ // DECLARED_METHODS_ALL), if it is OK to break NetworkCallbacks created using
+ // dexmaker-mockito-inline and either spy() or MockSettings.useConstructor (see
+ // comment in ConnectivityManager which sets the flag to DECLARED_METHODS_ALL).
+ Log.wtf(TAG, "requestNetwork called without declaredMethodsFlag from "
+ + callingPackageName);
+ declaredMethodsFlag = DECLARED_METHODS_ALL;
+ }
if (legacyType != TYPE_NONE && !hasNetworkStackPermission()) {
if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
callingPackageName)) {
@@ -8048,7 +8378,7 @@
nextNetworkRequestId(), reqType);
final NetworkRequestInfo nri = getNriToRegister(
asUid, networkRequest, messenger, binder, callbackFlags,
- callingAttributionTag);
+ callingAttributionTag, declaredMethodsFlag);
if (DBG) log("requestNetwork for " + nri);
trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_REQUEST, nri);
if (timeoutMs > 0) {
@@ -8074,7 +8404,7 @@
private NetworkRequestInfo getNriToRegister(final int asUid, @NonNull final NetworkRequest nr,
@Nullable final Messenger msgr, @Nullable final IBinder binder,
@NetworkCallback.Flag int callbackFlags,
- @Nullable String callingAttributionTag) {
+ @Nullable String callingAttributionTag, int declaredMethodsFlags) {
final List<NetworkRequest> requests;
if (NetworkRequest.Type.TRACK_DEFAULT == nr.type) {
requests = copyDefaultNetworkRequestsForUid(
@@ -8083,7 +8413,8 @@
requests = Collections.singletonList(nr);
}
return new NetworkRequestInfo(
- asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag);
+ asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag,
+ declaredMethodsFlags);
}
private boolean shouldCheckCapabilitiesDeclaration(
@@ -8220,21 +8551,13 @@
// Policy already enforced.
return;
}
- if (mDeps.isAtLeastV()) {
- if (mBpfNetMaps.isUidRestrictedOnMeteredNetworks(uid)) {
- // If UID is restricted, don't allow them to bring up metered APNs.
- networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
- }
- return;
- }
- final long ident = Binder.clearCallingIdentity();
- try {
- if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {
- // If UID is restricted, don't allow them to bring up metered APNs.
- networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
- }
- } finally {
- Binder.restoreCallingIdentity(ident);
+ final boolean isRestrictedOnMeteredNetworks = mDeps.isAtLeastV()
+ ? mBpfNetMaps.isUidRestrictedOnMeteredNetworks(uid)
+ : BinderUtils.withCleanCallingIdentity(() ->
+ mPolicyManager.isUidRestrictedOnMeteredNetworks(uid));
+ if (isRestrictedOnMeteredNetworks) {
+ // If UID is restricted, don't allow them to bring up metered APNs.
+ networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
}
}
@@ -8373,7 +8696,13 @@
public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities,
Messenger messenger, IBinder binder,
@NetworkCallback.Flag int callbackFlags,
- @NonNull String callingPackageName, @NonNull String callingAttributionTag) {
+ @NonNull String callingPackageName, @NonNull String callingAttributionTag,
+ int declaredMethodsFlag) {
+ if (declaredMethodsFlag == 0) {
+ Log.wtf(TAG, "listenForNetwork called without declaredMethodsFlag from "
+ + callingPackageName);
+ declaredMethodsFlag = DECLARED_METHODS_ALL;
+ }
final int callingUid = mDeps.getCallingUid();
if (!hasWifiNetworkListenPermission(networkCapabilities)) {
enforceAccessPermission();
@@ -8395,7 +8724,7 @@
NetworkRequest.Type.LISTEN);
NetworkRequestInfo nri =
new NetworkRequestInfo(callingUid, networkRequest, messenger, binder, callbackFlags,
- callingAttributionTag);
+ callingAttributionTag, declaredMethodsFlag);
if (VDBG) log("listenForNetwork for " + nri);
trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_LISTENER, nri);
@@ -8604,9 +8933,15 @@
@NonNull
final NetworkRequestInfo mDefaultRequest;
// Collection of NetworkRequestInfo's used for default networks.
+ // This set is read and iterated on multiple threads.
+ // Using CopyOnWriteArraySet since number of default network request is small (system default
+ // network request + per-app default network requests) and updated infrequently but read
+ // frequently.
@VisibleForTesting
@NonNull
- final ArraySet<NetworkRequestInfo> mDefaultNetworkRequests = new ArraySet<>();
+ final CopyOnWriteArraySet<NetworkRequestInfo> mDefaultNetworkRequests =
+ new CopyOnWriteArraySet<>();
+
private boolean isPerAppDefaultRequest(@NonNull final NetworkRequestInfo nri) {
return (mDefaultNetworkRequests.contains(nri) && mDefaultRequest != nri);
@@ -9095,7 +9430,7 @@
}
networkAgent.networkMonitor().notifyLinkPropertiesChanged(
new LinkProperties(newLp, true /* parcelSensitiveFields */));
- notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
+ notifyNetworkCallbacks(networkAgent, CALLBACK_IP_CHANGED);
}
mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
@@ -9343,8 +9678,10 @@
* interfaces.
* Ingress discard rule is added to the address iff
* 1. The address is not a link local address
- * 2. The address is used by a single VPN interface and not used by any other
- * interfaces even non-VPN ones
+ * 2. The address is used by a single interface of VPN whose VPN type is not TYPE_VPN_LEGACY
+ * or TYPE_VPN_OEM and the address is not used by any other interfaces even non-VPN ones
+ * Ingress discard rule is not be added to TYPE_VPN_LEGACY or TYPE_VPN_OEM VPN since these VPNs
+ * might need to receive packet to VPN address via non-VPN interface.
* This method can be called during network disconnects, when nai has already been removed from
* mNetworkAgentInfos.
*
@@ -9379,7 +9716,10 @@
// for different network.
final Set<Pair<InetAddress, String>> ingressDiscardRules = new ArraySet<>();
for (final NetworkAgentInfo agent : nais) {
- if (!agent.isVPN() || agent.isDestroyed()) {
+ final int vpnType = getVpnType(agent);
+ if (!agent.isVPN() || agent.isDestroyed()
+ || vpnType == VpnManager.TYPE_VPN_LEGACY
+ || vpnType == VpnManager.TYPE_VPN_OEM) {
continue;
}
final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
@@ -9591,8 +9931,7 @@
if (prevSuspended != suspended) {
// TODO (b/73132094) : remove this call once the few users of onSuspended and
// onResumed have been removed.
- notifyNetworkCallbacks(nai, suspended ? ConnectivityManager.CALLBACK_SUSPENDED
- : ConnectivityManager.CALLBACK_RESUMED);
+ notifyNetworkCallbacks(nai, suspended ? CALLBACK_SUSPENDED : CALLBACK_RESUMED);
}
if (prevSuspended != suspended || prevRoaming != roaming) {
// updateNetworkInfo will mix in the suspended info from the capabilities and
@@ -9679,7 +10018,7 @@
// If the requestable capabilities have changed or the score changed, we can't have been
// called by rematchNetworkAndRequests, so it's safe to start a rematch.
rematchAllNetworksAndRequests();
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ notifyNetworkCallbacks(nai, CALLBACK_CAP_CHANGED);
}
updateNetworkInfoForRoamingAndSuspended(nai, prevNc, newNc);
@@ -9821,7 +10160,7 @@
// But here there is no new request, so the rematch won't see anything. Send
// callbacks to apps now to tell them about the loss of upstream.
notifyNetworkCallbacks(nai,
- ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
return;
}
}
@@ -9837,7 +10176,8 @@
configBuilder.setUpstreamSelector(nr);
final NetworkRequestInfo nri = new NetworkRequestInfo(
nai.creatorUid, nr, null /* messenger */, null /* binder */,
- 0 /* callbackFlags */, null /* attributionTag */);
+ 0 /* callbackFlags */, null /* attributionTag */,
+ DECLARED_METHODS_NONE);
if (null != oldSatisfier) {
// Set the old satisfier in the new NRI so that the rematch will see any changes
nri.setSatisfier(oldSatisfier, nr);
@@ -10126,7 +10466,7 @@
private void sendPendingIntentForRequest(NetworkRequestInfo nri, NetworkAgentInfo networkAgent,
int notificationType) {
- if (notificationType == ConnectivityManager.CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
+ if (notificationType == CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
Intent intent = new Intent();
intent.putExtra(ConnectivityManager.EXTRA_NETWORK, networkAgent.network);
// If apps could file multi-layer requests with PendingIntents, they'd need to know
@@ -10196,6 +10536,18 @@
return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
}
+ private Bundle makeCommonBundleForCallback(@NonNull final NetworkRequestInfo nri,
+ @Nullable Network network) {
+ final Bundle bundle = new Bundle();
+ // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+ // TODO: check if defensive copies of data is needed.
+ putParcelable(bundle, nri.getNetworkRequestForCallback());
+ if (network != null) {
+ putParcelable(bundle, network);
+ }
+ return bundle;
+ }
+
// networkAgent is only allowed to be null if notificationType is
// CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
// available, while all other cases are about some particular network.
@@ -10208,19 +10560,24 @@
// are Type.LISTEN, but should not have NetworkCallbacks invoked.
return;
}
- final Bundle bundle = new Bundle();
- // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
- // TODO: check if defensive copies of data is needed.
- final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
- putParcelable(bundle, nrForCallback);
- Message msg = Message.obtain();
- if (notificationType != ConnectivityManager.CALLBACK_UNAVAIL) {
- putParcelable(bundle, networkAgent.network);
+ // Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
+ // queue callbacks before checking the declared methods flags.
+ if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
+ return;
}
+ if (!nri.isCallbackOverridden(notificationType)) {
+ // No need to send the notification as the recipient method is not overridden
+ return;
+ }
+ final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
+ ? null
+ : networkAgent.network;
+ final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
final boolean includeLocationSensitiveInfo =
(nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
+ final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
switch (notificationType) {
- case ConnectivityManager.CALLBACK_AVAILABLE: {
+ case CALLBACK_AVAILABLE: {
final NetworkCapabilities nc =
createWithLocationInfoSanitizedIfNecessaryWhenParceled(
networkCapabilitiesRestrictedForCallerPermissions(
@@ -10235,15 +10592,9 @@
// method here.
bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
localNetworkInfoForNai(networkAgent));
- // For this notification, arg1 contains the blocked status.
- msg.arg1 = arg1;
break;
}
- case ConnectivityManager.CALLBACK_LOSING: {
- msg.arg1 = arg1;
- break;
- }
- case ConnectivityManager.CALLBACK_CAP_CHANGED: {
+ case CALLBACK_CAP_CHANGED: {
// networkAgent can't be null as it has been accessed a few lines above.
final NetworkCapabilities netCap =
networkCapabilitiesRestrictedForCallerPermissions(
@@ -10256,17 +10607,16 @@
nri.mCallingAttributionTag));
break;
}
- case ConnectivityManager.CALLBACK_IP_CHANGED: {
+ case CALLBACK_IP_CHANGED: {
putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
networkAgent.linkProperties, nri.mPid, nri.mUid));
break;
}
- case ConnectivityManager.CALLBACK_BLK_CHANGED: {
+ case CALLBACK_BLK_CHANGED: {
maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1);
- msg.arg1 = arg1;
break;
}
- case ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+ case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
if (!networkAgent.isLocalNetwork()) {
Log.wtf(TAG, "Callback for local info for a non-local network");
return;
@@ -10275,17 +10625,26 @@
break;
}
}
+ callCallbackForRequest(nri, notificationType, bundle, arg1);
+ }
+
+ private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri, int notificationType,
+ Bundle bundle, int arg1) {
+ Message msg = Message.obtain();
+ msg.arg1 = arg1;
msg.what = notificationType;
msg.setData(bundle);
try {
if (VDBG) {
String notification = ConnectivityManager.getCallbackName(notificationType);
- log("sending notification " + notification + " for " + nrForCallback);
+ log("sending notification " + notification + " for "
+ + nri.getNetworkRequestForCallback());
}
nri.mMessenger.send(msg);
} catch (RemoteException e) {
// may occur naturally in the race of binder death.
- loge("RemoteException caught trying to send a callback msg for " + nrForCallback);
+ loge("RemoteException caught trying to send a callback msg for "
+ + nri.getNetworkRequestForCallback());
}
}
@@ -10535,7 +10894,7 @@
private void processListenRequests(@NonNull final NetworkAgentInfo nai) {
// For consistency with previous behaviour, send onLost callbacks before onAvailable.
processNewlyLostListenRequests(nai);
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ notifyNetworkCallbacks(nai, CALLBACK_CAP_CHANGED);
processNewlySatisfiedListenRequests(nai);
}
@@ -10548,7 +10907,7 @@
if (!nr.isListen()) continue;
if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) {
nai.removeRequest(nr.requestId);
- callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0);
+ callCallbackForRequest(nri, nai, CALLBACK_LOST, 0);
}
}
}
@@ -10880,7 +11239,7 @@
notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo);
} else {
callCallbackForRequest(event.mNetworkRequestInfo, event.mOldNetwork,
- ConnectivityManager.CALLBACK_LOST, 0);
+ CALLBACK_LOST, 0);
}
}
@@ -10924,7 +11283,7 @@
if (null != localInfoChangedAgents) {
for (final NetworkAgentInfo nai : localInfoChangedAgents) {
notifyNetworkCallbacks(nai,
- ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
}
}
@@ -10967,7 +11326,7 @@
if (Objects.equals(nai.networkCapabilities, newNc)) return;
updateNetworkPermissions(nai, newNc);
nai.getAndSetNetworkCapabilities(newNc);
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ notifyNetworkCallbacks(nai, CALLBACK_CAP_CHANGED);
}
private void updateLegacyTypeTrackerAndVpnLockdownForRematch(
@@ -11336,7 +11695,7 @@
rematchAllNetworksAndRequests();
// This has to happen after matching the requests, because callbacks are just requests.
- notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
+ notifyNetworkCallbacks(networkAgent, CALLBACK_PRECHECK);
} else if (state == NetworkInfo.State.DISCONNECTED) {
networkAgent.disconnect();
if (networkAgent.isVPN()) {
@@ -11369,22 +11728,18 @@
protected void notifyNetworkAvailable(NetworkAgentInfo nai, NetworkRequestInfo nri) {
mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_REQUEST, nri);
if (nri.mPendingIntent != null) {
- sendPendingIntentForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE);
+ sendPendingIntentForRequest(nri, nai, CALLBACK_AVAILABLE);
// Attempt no subsequent state pushes where intents are involved.
return;
}
- final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
- final boolean metered = nai.networkCapabilities.isMetered();
- final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
- callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE,
- getBlockedState(nri.mAsUid, blockedReasons, metered, vpnBlocked));
+ callCallbackForRequest(nri, nai, CALLBACK_AVAILABLE, getBlockedState(nai, nri.mAsUid));
}
// Notify the requests on this NAI that the network is now lingered.
private void notifyNetworkLosing(@NonNull final NetworkAgentInfo nai, final long now) {
final int lingerTime = (int) (nai.getInactivityExpiry() - now);
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+ notifyNetworkCallbacks(nai, CALLBACK_LOSING, lingerTime);
}
private int getPermissionBlockedState(final int uid, final int reasons) {
@@ -11408,6 +11763,13 @@
: reasons & ~BLOCKED_REASON_LOCKDOWN_VPN;
}
+ private int getBlockedState(@NonNull NetworkAgentInfo nai, int uid) {
+ final boolean metered = nai.networkCapabilities.isMetered();
+ final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
+ final int blockedReasons = mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE);
+ return getBlockedState(uid, blockedReasons, metered, vpnBlocked);
+ }
+
private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) {
if (blockedReasons == BLOCKED_REASON_NONE) {
mUidBlockedReasons.delete(uid);
@@ -11447,7 +11809,7 @@
final int newBlockedState = getBlockedState(
nri.mAsUid, blockedReasons, newMetered, newVpnBlocked);
if (oldBlockedState != newBlockedState) {
- callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+ callCallbackForRequest(nri, nai, CALLBACK_BLK_CHANGED,
newBlockedState);
}
}
@@ -11474,7 +11836,7 @@
NetworkRequest nr = nai.requestAt(i);
NetworkRequestInfo nri = mNetworkRequests.get(nr);
if (nri != null && nri.mAsUid == uid) {
- callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+ callCallbackForRequest(nri, nai, CALLBACK_BLK_CHANGED,
newBlockedState);
}
}
@@ -11832,6 +12194,10 @@
return 0;
}
case "get-package-networking-enabled": {
+ if (!mDeps.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ "This command is not supported on T-");
+ }
final String packageName = getNextArg();
final int rule = getPackageFirewallRule(
ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3, packageName);
@@ -11861,6 +12227,10 @@
return 0;
}
case "get-background-networking-enabled-for-uid": {
+ if (!mDeps.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ "This command is not supported on T-");
+ }
final Integer uid = parseIntegerArgument(getNextArg());
if (null == uid) {
onHelp();
@@ -13909,6 +14279,7 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private int getPackageFirewallRule(final int chain, final String packageName)
throws PackageManager.NameNotFoundException {
final PackageManager pm = mContext.getPackageManager();
@@ -13916,6 +14287,7 @@
return getUidFirewallRule(chain, appId);
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Override
public int getUidFirewallRule(final int chain, final int uid) {
enforceNetworkStackOrSettingsPermission();
@@ -14140,4 +14512,14 @@
}
return features;
}
+
+ @Override
+ public boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag) {
+ switch (featureFlag) {
+ case INGRESS_TO_VPN_ADDRESS_FILTERING:
+ return mIngressToVpnAddressFiltering;
+ default:
+ throw new IllegalArgumentException("Unknown flag: " + featureFlag);
+ }
+ }
}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index b1c770b..e808746 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -45,7 +45,6 @@
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.SingleWriterBpfMap;
import com.android.net.module.util.TcUtils;
import com.android.net.module.util.bpf.ClatEgress4Key;
import com.android.net.module.util.bpf.ClatEgress4Value;
@@ -257,7 +256,8 @@
@Nullable
public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
try {
- return SingleWriterBpfMap.getSingleton(CLAT_INGRESS6_MAP_PATH,
+ // written from clatd.c
+ return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
ClatIngress6Key.class, ClatIngress6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create ingress6 map: " + e);
@@ -269,7 +269,8 @@
@Nullable
public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
try {
- return SingleWriterBpfMap.getSingleton(CLAT_EGRESS4_MAP_PATH,
+ // written from clatd.c
+ return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
ClatEgress4Key.class, ClatEgress4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create egress4 map: " + e);
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 1ee1ed7..df87316 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -49,6 +49,9 @@
public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
"use_declared_methods_for_callbacks";
+ public static final String QUEUE_CALLBACKS_FOR_FROZEN_APPS =
+ "queue_callbacks_for_frozen_apps";
+
private boolean mNoRematchAllRequestsOnRegister;
/**
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index cf6127f..7a008c6 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -23,6 +23,7 @@
import android.os.Binder;
import android.os.Process;
import android.os.ServiceSpecificException;
+import android.os.UserHandle;
import android.system.ErrnoException;
import android.util.Log;
@@ -42,7 +43,7 @@
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
private static final String BLOCKED_PORTS_MAP_PATH =
- "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
+ "/sys/fs/bpf/netd_shared/map_netd_blocked_ports_map";
private final Context mContext;
@@ -67,8 +68,8 @@
}
private void enforceBlockPortPermission() {
- final int uid = Binder.getCallingUid();
- if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+ final int appId = UserHandle.getAppId(Binder.getCallingUid());
+ if (appId == Process.ROOT_UID || appId == Process.PHONE_UID) return;
PermissionUtils.enforceNetworkStackPermission(mContext);
}
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index ac02229..c940eec 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -29,6 +29,7 @@
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -41,6 +42,7 @@
import android.net.NetworkCapabilities;
import android.net.ResolverParamsParcel;
import android.net.Uri;
+import android.net.resolv.aidl.DohParamsParcel;
import android.net.shared.PrivateDnsConfig;
import android.os.Binder;
import android.os.RemoteException;
@@ -52,16 +54,17 @@
import android.util.Pair;
import java.net.InetAddress;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
/**
* Encapsulate the management of DNS settings for networks.
@@ -382,15 +385,14 @@
paramsParcel.domains = getDomainStrings(lp.getDomains());
paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
paramsParcel.tlsServers =
- strictMode ? makeStrings(
- Arrays.stream(privateDnsCfg.ips)
- .filter((ip) -> lp.isReachable(ip))
- .collect(Collectors.toList()))
+ strictMode ? makeStrings(getReachableAddressList(privateDnsCfg.ips, lp))
: useTls ? paramsParcel.servers // Opportunistic
: new String[0]; // Off
paramsParcel.transportTypes = nc.getTransportTypes();
paramsParcel.meteredNetwork = nc.isMetered();
paramsParcel.interfaceNames = lp.getAllInterfaceNames().toArray(new String[0]);
+ paramsParcel.dohParams = makeDohParamsParcel(privateDnsCfg, lp);
+
// Prepare to track the validation status of the DNS servers in the
// resolver config when private DNS is in opportunistic or strict mode.
if (useTls) {
@@ -403,21 +405,11 @@
mPrivateDnsValidationMap.remove(netId);
}
- Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
- + "%d, %d, %s, %s, %s, %b, %s)", paramsParcel.netId,
- Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
- paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
- paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
- paramsParcel.retryCount, paramsParcel.tlsName,
- Arrays.toString(paramsParcel.tlsServers),
- Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork,
- Arrays.toString(paramsParcel.interfaceNames)));
-
+ Log.d(TAG, "sendDnsConfigurationForNetwork(" + paramsParcel + ")");
try {
mDnsResolver.setResolverConfiguration(paramsParcel);
} catch (RemoteException | ServiceSpecificException e) {
Log.e(TAG, "Error setting DNS configuration: " + e);
- return;
}
}
@@ -498,4 +490,29 @@
private static String[] getDomainStrings(String domains) {
return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
}
+
+ @NonNull
+ private List<InetAddress> getReachableAddressList(@NonNull InetAddress[] ips,
+ @NonNull LinkProperties lp) {
+ final ArrayList<InetAddress> out = new ArrayList<InetAddress>(Arrays.asList(ips));
+ out.removeIf(ip -> !lp.isReachable(ip));
+ return out;
+ }
+
+ @Nullable
+ private DohParamsParcel makeDohParamsParcel(@NonNull PrivateDnsConfig cfg,
+ @NonNull LinkProperties lp) {
+ if (!cfg.ddrEnabled) {
+ return null;
+ }
+ if (cfg.mode == PRIVATE_DNS_MODE_OFF) {
+ return new DohParamsParcel.Builder().build();
+ }
+ return new DohParamsParcel.Builder()
+ .setName(cfg.dohName)
+ .setIps(makeStrings(getReachableAddressList(cfg.dohIps, lp)))
+ .setDohpath(cfg.dohPath)
+ .setPort(cfg.dohPort)
+ .build();
+ }
}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index 7b11eda..a9100ac 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -55,13 +55,17 @@
@Field(order = 7, type = Type.S8)
public final byte dscp;
- @Field(order = 8, type = Type.U8, padding = 3)
- public final short mask;
+ @Field(order = 8, type = Type.Bool)
+ public final boolean match_src_ip;
- private static final int SRC_IP_MASK = 0x1;
- private static final int DST_IP_MASK = 0x02;
- private static final int SRC_PORT_MASK = 0x4;
- private static final int PROTO_MASK = 0x8;
+ @Field(order = 9, type = Type.Bool)
+ public final boolean match_dst_ip;
+
+ @Field(order = 10, type = Type.Bool)
+ public final boolean match_src_port;
+
+ @Field(order = 11, type = Type.Bool)
+ public final boolean match_proto;
private boolean ipEmpty(final byte[] ip) {
for (int i = 0; i < ip.length; i++) {
@@ -98,24 +102,6 @@
private static final byte[] EMPTY_ADDRESS_FIELD =
InetAddress.parseNumericAddress("::").getAddress();
- private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort,
- final int dstPortStart, final short proto, final byte dscp) {
- short mask = 0;
- if (src46 != EMPTY_ADDRESS_FIELD) {
- mask |= SRC_IP_MASK;
- }
- if (dst46 != EMPTY_ADDRESS_FIELD) {
- mask |= DST_IP_MASK;
- }
- if (srcPort != -1) {
- mask |= SRC_PORT_MASK;
- }
- if (proto != -1) {
- mask |= PROTO_MASK;
- }
- return mask;
- }
-
private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int ifIndex,
final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
final byte dscp) {
@@ -131,9 +117,10 @@
this.proto = proto != -1 ? proto : 0;
this.dscp = dscp;
- // Use member variables for IP since byte[] is needed and api variables for everything else
- // so -1 is passed into mask if parameter is not present.
- this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
+ this.match_src_ip = (src46 != null);
+ this.match_dst_ip = (dst46 != null);
+ this.match_src_port = (srcPort != -1);
+ this.match_proto = (proto != -1);
}
public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int ifIndex,
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 3db37e5..8a2e72c 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -567,7 +567,9 @@
@Override
public void close() {
- IoUtils.closeQuietly(mFileDescriptor);
+ if (mFileDescriptor != null) {
+ IoUtils.closeQuietly(mFileDescriptor);
+ }
}
}
@@ -611,6 +613,7 @@
setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
+ close();
return;
}
mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
@@ -695,6 +698,7 @@
NetworkConstants.DNS_SERVER_PORT);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
+ close();
return;
}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index e2834b0..85258f8 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -39,12 +39,13 @@
"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/FeatureVersions.java",
+ "device/com/android/net/module/util/HandlerUtils.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",
+ "device/com/android/net/module/util/SyncStateMachine.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",
@@ -57,23 +58,19 @@
"//apex_available:platform",
],
visibility: [
- "//frameworks/base/packages/Tethering",
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/Connectivity/framework:__subpackages__",
- "//frameworks/opt/net/ike",
- "//frameworks/opt/net/wifi/service",
- "//packages/modules/Wifi/service",
- "//frameworks/opt/net/telephony",
"//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
],
static_libs: [
+ "modules-utils-statemachine",
"net-utils-framework-common",
],
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
],
lint: {
@@ -267,7 +264,7 @@
],
libs: [
"framework-annotations-lib",
- "framework-connectivity",
+ "framework-connectivity.stubs.module_lib",
],
static_libs: [
"net-utils-device-common",
@@ -280,7 +277,7 @@
"//apex_available:platform",
],
lint: {
- baseline_filename: "lint-baseline.xml",
+ strict_updatability_linting: true,
error_checks: ["NewApi"],
},
}
@@ -343,12 +340,10 @@
],
sdk_version: "module_current",
min_sdk_version: "30",
- static_libs: [
- "modules-utils-build_system",
- ],
libs: [
"framework-annotations-lib",
- "framework-connectivity",
+ "framework-connectivity.stubs.module_lib",
+ "modules-utils-build_system",
],
// TODO: remove "apex_available:platform".
apex_available: [
@@ -443,6 +438,7 @@
"device/com/android/net/module/util/SharedLog.java",
"framework/com/android/net/module/util/ByteUtils.java",
"framework/com/android/net/module/util/CollectionUtils.java",
+ "framework/com/android/net/module/util/DnsUtils.java",
"framework/com/android/net/module/util/HexDump.java",
"framework/com/android/net/module/util/LinkPropertiesUtils.java",
],
@@ -454,6 +450,149 @@
visibility: ["//packages/modules/Connectivity/service-t"],
}
+// net-utils-framework-connectivity is only for framework-connectivity.
+java_library {
+ name: "net-utils-framework-connectivity",
+ srcs: [
+ ":net-utils-framework-connectivity-srcs",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity/framework",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib",
+ "framework-configinfrastructure.stubs.module_lib",
+ "framework-connectivity.stubs.module_lib",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
+}
+
+java_defaults {
+ name: "net-utils-non-bootclasspath-defaults",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib",
+ "framework-configinfrastructure.stubs.module_lib",
+ "framework-connectivity.stubs.module_lib",
+ "framework-connectivity-t.stubs.module_lib",
+ "framework-location.stubs.module_lib",
+ "framework-tethering.stubs.module_lib",
+ "unsupportedappusage",
+ ],
+ static_libs: [
+ "modules-utils-build_system",
+ "modules-utils-statemachine",
+ "net-utils-non-bootclasspath-aidl-java",
+ "netd-client",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ defaults_visibility: [
+ "//visibility:private",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
+}
+
+// net-utils-service-connectivity is only for service-connectivity.
+java_library {
+ name: "net-utils-service-connectivity",
+ srcs: [
+ ":net-utils-all-srcs",
+ ],
+ exclude_srcs: [
+ ":net-utils-framework-connectivity-srcs",
+ ],
+ libs: [
+ "net-utils-framework-connectivity",
+ "framework-connectivity.impl",
+ "framework-tethering.impl",
+ ],
+ defaults: ["net-utils-non-bootclasspath-defaults"],
+ jarjar_rules: "jarjar-rules-shared.txt",
+ visibility: [
+ "//packages/modules/Connectivity/service",
+ "//packages/modules/Connectivity/staticlibs/tests/unit",
+ ],
+}
+
+// net-utils-connectivity-apks is only for NetworkStack, CaptivePortalLogin and
+// Tethering.apk
+// It includes all the static libraries in this directory, which is safe because
+// these APKs use R8 to strip out unused code, and they do not depend on
+// bootclasspath jars that may have duplicate copies of the included classes
+// with the same jarjaring.
+// Tethering.apk does depend on a bootclasspath jar (framework-tethering.jar),
+// however it does not use any of the static libraries. If it did, Tethering.apk
+// would need to use another variant that excludes classes that are already
+// included in framework-tethering.jar (similarly to how framework-connectivity
+// and service-connectivity do it). Otherwise, static libs included in
+// framework-tethering and Tethering.apk and jarjared the same way would
+// conflict.
+java_library {
+ name: "net-utils-connectivity-apks",
+ srcs: [
+ ":net-utils-all-srcs",
+ ":framework-connectivity-shared-srcs",
+ ],
+ defaults: ["net-utils-non-bootclasspath-defaults"],
+ jarjar_rules: "jarjar-rules-shared.txt",
+ visibility: [
+ "//packages/modules/CaptivePortalLogin:__subpackages__",
+ "//packages/modules/Connectivity/Tethering",
+ ],
+}
+
+aidl_interface {
+ name: "net-utils-non-bootclasspath-aidl",
+ srcs: [
+ ":net-utils-aidl-srcs",
+ ],
+ unstable: true,
+ backend: {
+ java: {
+ enabled: true,
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ "com.android.wifi",
+ "//apex_available:platform",
+ ],
+ },
+ cpp: {
+ enabled: false,
+ },
+ ndk: {
+ enabled: false,
+ },
+ rust: {
+ enabled: false,
+ },
+ },
+ include_dirs: [
+ "packages/modules/Connectivity/framework/aidl-export",
+ ],
+ visibility: [
+ "//system/tools/aidl/build",
+ ],
+}
+
// Use a filegroup and not a library for telephony sources, as framework-annotations cannot be
// included either (some annotations would be duplicated on the bootclasspath).
filegroup {
@@ -471,8 +610,7 @@
],
}
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
+// Filegroup to build lib used by Wifi framework
// Any class here *must* have a corresponding jarjar rule in the wifi build rules.
filegroup {
name: "net-utils-framework-wifi-common-srcs",
@@ -484,24 +622,78 @@
"framework/com/android/net/module/util/NetUtils.java",
],
path: "framework",
+ visibility: ["//visibility:private"],
+}
+
+// Use a file group containing classes necessary for framework-connectivity. The file group should
+// be as small as possible because because the classes end up in the bootclasspath and R8 is not
+// used to remove unused classes.
+filegroup {
+ name: "net-utils-framework-connectivity-srcs",
+ srcs: [
+ "device/com/android/net/module/util/BpfBitmap.java",
+ "device/com/android/net/module/util/BpfDump.java",
+ "device/com/android/net/module/util/BpfMap.java",
+ "device/com/android/net/module/util/BpfUtils.java",
+ "device/com/android/net/module/util/IBpfMap.java",
+ "device/com/android/net/module/util/JniUtil.java",
+ "device/com/android/net/module/util/SingleWriterBpfMap.java",
+ "device/com/android/net/module/util/Struct.java",
+ "device/com/android/net/module/util/TcUtils.java",
+ "framework/com/android/net/module/util/HexDump.java",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+filegroup {
+ name: "net-utils-all-srcs",
+ srcs: [
+ "device/**/*.java",
+ ":net-utils-framework-common-srcs",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+filegroup {
+ name: "net-utils-aidl-srcs",
+ srcs: [
+ "device/**/*.aidl",
+ ],
+ path: "device",
+ visibility: ["//visibility:private"],
+}
+
+java_library {
+ name: "net-utils-service-wifi",
+ srcs: [
+ ":net-utils-all-srcs",
+ ],
+ exclude_srcs: [":net-utils-framework-wifi-common-srcs"],
+ libs: [
+ "net-utils-framework-wifi",
+ ],
+ defaults: ["net-utils-non-bootclasspath-defaults"],
+
visibility: [
- "//frameworks/base",
+ "//packages/modules/Wifi/service",
+ ],
+ apex_available: [
+ "com.android.wifi",
],
}
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
-// Any class here *must* have a corresponding jarjar rule in the wifi build rules.
-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",
+java_library {
+ name: "net-utils-framework-wifi",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ srcs: [":net-utils-framework-wifi-common-srcs"],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity.stubs.module_lib",
+ "unsupportedappusage",
],
visibility: [
- "//frameworks/opt/net/wifi/service",
- "//packages/modules/Wifi/service",
+ "//packages/modules/Wifi/framework",
],
+ apex_available: ["com.android.wifi"],
}
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 7aafd69..79234f5 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -17,8 +17,8 @@
"netd-client",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs.system",
+ "android.test.base.stubs.system",
],
visibility: [
// Visible for Tethering and NetworkStack integration test and link NetdStaticLibTestsLib
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 0426ace..04ce2fa 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -22,6 +22,7 @@
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.DNS_RESOLVER_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.MODULE_MASK;
import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.VERSION_MASK;
@@ -68,7 +69,8 @@
@VisibleForTesting
public static void resetPackageVersionCacheForTest() {
sPackageVersion = -1;
- sModuleVersion = -1;
+ sTetheringModuleVersion = -1;
+ sResolvModuleVersion = -1;
sNetworkStackModuleVersion = -1;
}
@@ -243,23 +245,23 @@
}
}
- // Guess the tethering module name based on the package prefix of the connectivity resources
- // Take the resource package name, cut it before "connectivity" and append "tethering".
+ // Guess an APEX module name based on the package prefix of the connectivity resources
+ // Take the resource package name, cut it before "connectivity" and append the module name.
// Then resolve that package version number with packageManager.
- // If that fails retry by appending "go.tethering" instead
- private static long resolveTetheringModuleVersion(@NonNull Context context)
+ // If that fails retry by appending "go.<moduleName>" instead.
+ private static long resolveApexModuleVersion(@NonNull Context context, String moduleName)
throws PackageManager.NameNotFoundException {
final String pkgPrefix = resolvePkgPrefix(context);
final PackageManager packageManager = context.getPackageManager();
try {
- return packageManager.getPackageInfo(pkgPrefix + "tethering",
+ return packageManager.getPackageInfo(pkgPrefix + moduleName,
PackageManager.MATCH_APEX).getLongVersionCode();
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Device is using go modules");
// fall through
}
- return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
+ return packageManager.getPackageInfo(pkgPrefix + "go." + moduleName,
PackageManager.MATCH_APEX).getLongVersionCode();
}
@@ -274,19 +276,35 @@
return connResourcesPackage.substring(0, pkgPrefixLen);
}
- private static volatile long sModuleVersion = -1;
+ private static volatile long sTetheringModuleVersion = -1;
+
private static long getTetheringModuleVersion(@NonNull Context context) {
- if (sModuleVersion >= 0) return sModuleVersion;
+ if (sTetheringModuleVersion >= 0) return sTetheringModuleVersion;
try {
- sModuleVersion = resolveTetheringModuleVersion(context);
+ sTetheringModuleVersion = resolveApexModuleVersion(context, "tethering");
} catch (PackageManager.NameNotFoundException e) {
// It's expected to fail tethering module version resolution on the devices with
// flattened apex
Log.e(TAG, "Failed to resolve tethering module version: " + e);
return DEFAULT_PACKAGE_VERSION;
}
- return sModuleVersion;
+ return sTetheringModuleVersion;
+ }
+
+ private static volatile long sResolvModuleVersion = -1;
+ private static long getResolvModuleVersion(@NonNull Context context) {
+ if (sResolvModuleVersion >= 0) return sResolvModuleVersion;
+
+ try {
+ sResolvModuleVersion = resolveApexModuleVersion(context, "resolv");
+ } catch (PackageManager.NameNotFoundException e) {
+ // It's expected to fail resolv module version resolution on the devices with
+ // flattened apex
+ Log.e(TAG, "Failed to resolve resolv module version: " + e);
+ return DEFAULT_PACKAGE_VERSION;
+ }
+ return sResolvModuleVersion;
}
private static volatile long sNetworkStackModuleVersion = -1;
@@ -342,6 +360,8 @@
moduleVersion = getTetheringModuleVersion(context);
} else if (moduleId == NETWORK_STACK_MODULE_ID) {
moduleVersion = getNetworkStackModuleVersion(context);
+ } else if (moduleId == DNS_RESOLVER_MODULE_ID) {
+ moduleVersion = getResolvModuleVersion(context);
} else {
throw new IllegalArgumentException("Unknown module " + moduleId);
}
diff --git a/staticlibs/device/com/android/net/module/util/FeatureVersions.java b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
index d5f8124..d0cf3fd 100644
--- a/staticlibs/device/com/android/net/module/util/FeatureVersions.java
+++ b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
@@ -37,6 +37,7 @@
public static final long VERSION_MASK = 0x00F_FFFF_FFFFL;
public static final long CONNECTIVITY_MODULE_ID = 0x01L << MODULE_SHIFT;
public static final long NETWORK_STACK_MODULE_ID = 0x02L << MODULE_SHIFT;
+ public static final long DNS_RESOLVER_MODULE_ID = 0x03L << MODULE_SHIFT;
// CLAT_ADDRESS_TRANSLATE is a feature of the network stack, which doesn't throw when system
// try to add a NAT-T keepalive packet filter with v6 address, introduced in version
// M-2023-Sept on July 3rd, 2023.
@@ -48,4 +49,11 @@
// by BPF for the given uid and conditions, introduced in version M-2024-Feb on Nov 6, 2023.
public static final long FEATURE_IS_UID_NETWORKING_BLOCKED =
CONNECTIVITY_MODULE_ID + 34_14_00_000L;
+
+ // DDR is a feature implemented across NetworkStack, ConnectivityService and DnsResolver.
+ // The flag that enables this feature is in NetworkStack.
+ public static final long FEATURE_DDR_IN_CONNECTIVITY =
+ CONNECTIVITY_MODULE_ID + 35_11_00_000L;
+ public static final long FEATURE_DDR_IN_DNSRESOLVER =
+ DNS_RESOLVER_MODULE_ID + 35_11_00_000L;
}
diff --git a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
new file mode 100644
index 0000000..d47738b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.StringJoiner;
+import java.util.function.IntConsumer;
+import java.util.function.IntPredicate;
+
+/**
+ * A growing array of primitive ints.
+ *
+ * <p>This is similar to ArrayList<Integer>, but avoids the cost of boxing (each Integer costs
+ * 16 bytes) and creation / garbage collection of individual Integer objects.
+ *
+ * <p>This class does not use any heuristic for growing capacity, so every call to
+ * {@link #add(int)} may reallocate the backing array. Callers should use
+ * {@link #ensureHasCapacity(int)} to minimize this behavior when they plan to add several values.
+ */
+public class GrowingIntArray {
+ private int[] mValues;
+ private int mLength;
+
+ /**
+ * Create an empty GrowingIntArray with the given capacity.
+ */
+ public GrowingIntArray(int initialCapacity) {
+ mValues = new int[initialCapacity];
+ mLength = 0;
+ }
+
+ /**
+ * Create a GrowingIntArray with an initial array of values.
+ *
+ * <p>The array will be used as-is and may be modified, so callers must stop using it after
+ * calling this constructor.
+ */
+ protected GrowingIntArray(int[] initialValues) {
+ mValues = initialValues;
+ mLength = initialValues.length;
+ }
+
+ /**
+ * Add a value to the array.
+ */
+ public void add(int value) {
+ ensureHasCapacity(1);
+ mValues[mLength] = value;
+ mLength++;
+ }
+
+ /**
+ * Get the current number of values in the array.
+ */
+ public int length() {
+ return mLength;
+ }
+
+ /**
+ * Get the value at a given index.
+ *
+ * @throws ArrayIndexOutOfBoundsException if the index is out of bounds.
+ */
+ public int get(int index) {
+ if (index < 0 || index >= mLength) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mValues[index];
+ }
+
+ /**
+ * Iterate over all values in the array.
+ */
+ public void forEach(@NonNull IntConsumer consumer) {
+ for (int i = 0; i < mLength; i++) {
+ consumer.accept(mValues[i]);
+ }
+ }
+
+ /**
+ * Remove all values matching a predicate.
+ *
+ * @return true if at least one value was removed.
+ */
+ public boolean removeValues(@NonNull IntPredicate predicate) {
+ int newQueueLength = 0;
+ for (int i = 0; i < mLength; i++) {
+ final int cb = mValues[i];
+ if (!predicate.test(cb)) {
+ mValues[newQueueLength] = cb;
+ newQueueLength++;
+ }
+ }
+ if (mLength != newQueueLength) {
+ mLength = newQueueLength;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Indicates whether the array contains the given value.
+ */
+ public boolean contains(int value) {
+ for (int i = 0; i < mLength; i++) {
+ if (mValues[i] == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove all values from the array.
+ */
+ public void clear() {
+ mLength = 0;
+ }
+
+ /**
+ * Ensure at least the given number of values can be added to the array without reallocating.
+ *
+ * @param capacity The minimum number of additional values the array must be able to hold.
+ */
+ public void ensureHasCapacity(int capacity) {
+ if (mValues.length >= mLength + capacity) {
+ return;
+ }
+ mValues = Arrays.copyOf(mValues, mLength + capacity);
+ }
+
+ @VisibleForTesting
+ int getBackingArrayLength() {
+ return mValues.length;
+ }
+
+ /**
+ * Shrink the array backing this class to the minimum required length.
+ */
+ public void shrinkToLength() {
+ if (mValues.length != mLength) {
+ mValues = Arrays.copyOf(mValues, mLength);
+ }
+ }
+
+ /**
+ * Get values as array by shrinking the internal array to length and returning it.
+ *
+ * <p>This avoids reallocations if the array is already the correct length, but callers should
+ * stop using this instance of {@link GrowingIntArray} if they use the array returned by this
+ * method.
+ */
+ public int[] getMinimizedBackingArray() {
+ shrinkToLength();
+ return mValues;
+ }
+
+ /**
+ * Get the String representation of an item in the array, for use by {@link #toString()}.
+ */
+ protected String valueToString(int item) {
+ return String.valueOf(item);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringJoiner joiner = new StringJoiner(",", "[", "]");
+ forEach(item -> joiner.add(valueToString(item)));
+ return joiner.toString();
+ }
+}
diff --git a/framework/src/android/net/IRoutingCoordinator.aidl b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
similarity index 89%
rename from framework/src/android/net/IRoutingCoordinator.aidl
rename to staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
index cf02ec4..72a4a94 100644
--- a/framework/src/android/net/IRoutingCoordinator.aidl
+++ b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-package android.net;
+package com.android.net.module.util;
import android.net.RouteInfo;
/** @hide */
+// TODO: b/350630377 - This @Descriptor annotation workaround is to prevent the DESCRIPTOR from
+// being jarjared which changes the DESCRIPTOR and casues "java.lang.SecurityException: Binder
+// invocation to an incorrect interface" when calling the IPC.
+@Descriptor("value=no.jarjar.com.android.net.module.util.IRoutingCoordinator")
interface IRoutingCoordinator {
/**
* Add a route for specific network
diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
index 33e5bfa..a2dbd81 100644
--- a/staticlibs/device/com/android/net/module/util/PacketBuilder.java
+++ b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
@@ -24,10 +24,13 @@
import static com.android.net.module.util.IpUtils.ipChecksum;
import static com.android.net.module.util.IpUtils.tcpChecksum;
import static com.android.net.module.util.IpUtils.udpChecksum;
+import static com.android.net.module.util.NetworkStackConstants.IPPROTO_FRAGMENT;
import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_FRAGMENT_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.IPV6_LEN_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.TCP_CHECKSUM_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.UDP_CHECKSUM_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.UDP_LENGTH_OFFSET;
@@ -37,6 +40,7 @@
import androidx.annotation.NonNull;
import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.FragmentHeader;
import com.android.net.module.util.structs.Ipv4Header;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.net.module.util.structs.TcpHeader;
@@ -47,6 +51,10 @@
import java.net.Inet6Address;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
/**
* The class is used to build a packet.
@@ -210,6 +218,20 @@
}
}
+ private void writeFragmentHeader(ByteBuffer buffer, short nextHeader, int offset,
+ boolean mFlag, int id) throws IOException {
+ if ((offset & 7) != 0) {
+ throw new IOException("Invalid offset value, must be multiple of 8");
+ }
+ final FragmentHeader fragmentHeader = new FragmentHeader(nextHeader,
+ offset | (mFlag ? 1 : 0), id);
+ try {
+ fragmentHeader.writeToByteBuffer(buffer);
+ } catch (IllegalArgumentException | BufferOverflowException e) {
+ throw new IOException("Error writing to buffer: ", e);
+ }
+ }
+
/**
* Finalize the packet.
*
@@ -219,9 +241,31 @@
*/
@NonNull
public ByteBuffer finalizePacket() throws IOException {
+ // If the packet is finalized with L2 mtu greater than or equal to its current size, it will
+ // either return a List of size 1 or throw an IOException if something goes wrong.
+ return finalizePacket(mBuffer.position()).get(0);
+ }
+
+ /**
+ * Finalizes the packet with specified link MTU.
+ *
+ * Call after writing L4 header (no payload) or L4 payload to the buffer used by the builder.
+ * L3 header length, L3 header checksum and L4 header checksum are calculated and written back
+ * after finalization.
+ *
+ * @param l2mtu the maximum size, in bytes, of each individual packet. If the packet size
+ * exceeds the l2mtu, it will be fragmented into smaller packets.
+ * @return a list of packet(s), each containing a portion of the original L3 payload.
+ */
+ @NonNull
+ public List<ByteBuffer> finalizePacket(int l2mtu) throws IOException {
// [1] Finalize IPv4 or IPv6 header.
int ipHeaderOffset = INVALID_OFFSET;
if (mIpv4HeaderOffset != INVALID_OFFSET) {
+ if (mBuffer.position() > l2mtu) {
+ throw new IOException("IPv4 fragmentation is not supported");
+ }
+
ipHeaderOffset = mIpv4HeaderOffset;
// Populate the IPv4 totalLength field.
@@ -243,12 +287,15 @@
}
// [2] Finalize TCP or UDP header.
+ final int ipPayloadOffset;
if (mTcpHeaderOffset != INVALID_OFFSET) {
+ ipPayloadOffset = mTcpHeaderOffset;
// Populate the TCP header checksum field.
mBuffer.putShort(mTcpHeaderOffset + TCP_CHECKSUM_OFFSET, tcpChecksum(mBuffer,
ipHeaderOffset /* ipOffset */, mTcpHeaderOffset /* transportOffset */,
mBuffer.position() - mTcpHeaderOffset /* transportLen */));
} else if (mUdpHeaderOffset != INVALID_OFFSET) {
+ ipPayloadOffset = mUdpHeaderOffset;
// Populate the UDP header length field.
mBuffer.putShort(mUdpHeaderOffset + UDP_LENGTH_OFFSET,
(short) (mBuffer.position() - mUdpHeaderOffset));
@@ -257,11 +304,81 @@
mBuffer.putShort(mUdpHeaderOffset + UDP_CHECKSUM_OFFSET, udpChecksum(mBuffer,
ipHeaderOffset /* ipOffset */, mUdpHeaderOffset /* transportOffset */));
} else {
- throw new IOException("Packet is missing neither TCP nor UDP header");
+ throw new IOException("Packet has neither TCP nor UDP header");
}
- mBuffer.flip();
- return mBuffer;
+ if (mBuffer.position() <= l2mtu) {
+ mBuffer.flip();
+ return Arrays.asList(mBuffer);
+ }
+
+ // IPv6 Packet is fragmented into multiple smaller packets that would fit within the link
+ // MTU.
+ // Refer to https://tools.ietf.org/html/rfc2460
+ //
+ // original packet:
+ // +------------------+--------------+--------------+--//--+----------+
+ // | Unfragmentable | first | second | | last |
+ // | Part | fragment | fragment | .... | fragment |
+ // +------------------+--------------+--------------+--//--+----------+
+ //
+ // fragment packets:
+ // +------------------+--------+--------------+
+ // | Unfragmentable |Fragment| first |
+ // | Part | Header | fragment |
+ // +------------------+--------+--------------+
+ //
+ // +------------------+--------+--------------+
+ // | Unfragmentable |Fragment| second |
+ // | Part | Header | fragment |
+ // +------------------+--------+--------------+
+ // o
+ // o
+ // o
+ // +------------------+--------+----------+
+ // | Unfragmentable |Fragment| last |
+ // | Part | Header | fragment |
+ // +------------------+--------+----------+
+ final List<ByteBuffer> fragments = new ArrayList<>();
+ final int totalPayloadLen = mBuffer.position() - ipPayloadOffset;
+ final int perPacketPayloadLen = l2mtu - ipPayloadOffset - IPV6_FRAGMENT_HEADER_LEN;
+ final short protocol = (short) Byte.toUnsignedInt(
+ mBuffer.get(mIpv6HeaderOffset + IPV6_PROTOCOL_OFFSET));
+ Random random = new Random();
+ final int id = random.nextInt(Integer.MAX_VALUE);
+ int startOffset = 0;
+ // Copy the packet content to a byte array.
+ byte[] packet = new byte[mBuffer.position()];
+ // The ByteBuffer#get(int index, byte[] dst) method is only available in API level 35 and
+ // above. Here, we use a more primitive approach: reposition the ByteBuffer to the beginning
+ // before copying, then return its position to the end afterward.
+ mBuffer.position(0);
+ mBuffer.get(packet);
+ mBuffer.position(packet.length);
+ while (startOffset < totalPayloadLen) {
+ int copyPayloadLen = Math.min(perPacketPayloadLen, totalPayloadLen - startOffset);
+ // The data portion must be broken into segments aligned with 8-octet boundaries.
+ // Therefore, the payload length should be a multiple of 8 bytes for all fragments
+ // except the last one.
+ // See https://datatracker.ietf.org/doc/html/rfc791 section 3.2
+ if (copyPayloadLen != totalPayloadLen - startOffset) {
+ copyPayloadLen &= ~7;
+ }
+ ByteBuffer fragment = ByteBuffer.allocate(ipPayloadOffset + IPV6_FRAGMENT_HEADER_LEN
+ + copyPayloadLen);
+ fragment.put(packet, 0, ipPayloadOffset);
+ writeFragmentHeader(fragment, protocol, startOffset,
+ startOffset + copyPayloadLen < totalPayloadLen, id);
+ fragment.put(packet, ipPayloadOffset + startOffset, copyPayloadLen);
+ fragment.putShort(mIpv6HeaderOffset + IPV6_LEN_OFFSET,
+ (short) (IPV6_FRAGMENT_HEADER_LEN + copyPayloadLen));
+ fragment.put(mIpv6HeaderOffset + IPV6_PROTOCOL_OFFSET, (byte) IPPROTO_FRAGMENT);
+ fragment.flip();
+ fragments.add(fragment);
+ startOffset += copyPayloadLen;
+ }
+
+ return fragments;
}
/**
diff --git a/framework/src/android/net/RoutingCoordinatorManager.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
similarity index 95%
rename from framework/src/android/net/RoutingCoordinatorManager.java
rename to staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
index a9e7eef..02e3643 100644
--- a/framework/src/android/net/RoutingCoordinatorManager.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package android.net;
+package com.android.net.module.util;
import android.content.Context;
-import android.os.Build;
+import android.net.RouteInfo;
+import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
/**
* A manager class for talking to the routing coordinator service.
@@ -30,15 +30,14 @@
* by the build rules. Do not change build rules to gain access to this class from elsewhere.
* @hide
*/
-@RequiresApi(Build.VERSION_CODES.S)
public class RoutingCoordinatorManager {
@NonNull final Context mContext;
@NonNull final IRoutingCoordinator mService;
public RoutingCoordinatorManager(@NonNull final Context context,
- @NonNull final IRoutingCoordinator service) {
+ @NonNull final IBinder binder) {
mContext = context;
- mService = service;
+ mService = IRoutingCoordinator.Stub.asInterface(binder);
}
/**
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
similarity index 98%
rename from service/src/com/android/server/connectivity/RoutingCoordinatorService.java
rename to staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
index 742a2cc..c75b860 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.server.connectivity;
+package com.android.net.module.util;
import static com.android.net.module.util.NetdUtils.toRouteInfoParcel;
import android.annotation.NonNull;
import android.net.INetd;
-import android.net.IRoutingCoordinator;
+
import android.net.RouteInfo;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
diff --git a/staticlibs/device/com/android/net/module/util/SocketUtils.java b/staticlibs/device/com/android/net/module/util/SocketUtils.java
index 5e6a6c6..51671a6 100644
--- a/staticlibs/device/com/android/net/module/util/SocketUtils.java
+++ b/staticlibs/device/com/android/net/module/util/SocketUtils.java
@@ -19,8 +19,7 @@
import static android.net.util.SocketUtils.closeSocket;
import android.annotation.NonNull;
-import android.annotation.RequiresApi;
-import android.os.Build;
+import android.annotation.SuppressLint;
import android.system.NetlinkSocketAddress;
import java.io.FileDescriptor;
@@ -41,7 +40,11 @@
/**
* Make a socket address to communicate with netlink.
*/
- @NonNull @RequiresApi(Build.VERSION_CODES.S)
+ // NetlinkSocketAddress was CorePlatformApi on R and linter warns this is available on S+.
+ // android.net.util.SocketUtils.makeNetlinkSocketAddress can be used instead, but this method
+ // has been used on R, so suppress the linter and keep as it is.
+ @SuppressLint("NewApi")
+ @NonNull
public static SocketAddress makeNetlinkSocketAddress(int portId, int groupsMask) {
return new NetlinkSocketAddress(portId, groupsMask);
}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index ff7a711..69ca678 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -105,6 +105,7 @@
*/
public class Struct {
public enum Type {
+ Bool, // bool, size = 1 byte
U8, // unsigned byte, size = 1 byte
U16, // unsigned short, size = 2 bytes
U32, // unsigned int, size = 4 bytes
@@ -169,6 +170,9 @@
private static void checkAnnotationType(final Field annotation, final Class fieldType) {
switch (annotation.type()) {
+ case Bool:
+ if (fieldType == Boolean.TYPE) return;
+ break;
case U8:
case S16:
if (fieldType == Short.TYPE) return;
@@ -218,6 +222,7 @@
private static int getFieldLength(final Field annotation) {
int length = 0;
switch (annotation.type()) {
+ case Bool:
case U8:
case S8:
length = 1;
@@ -357,6 +362,9 @@
final Object value;
checkAnnotationType(fieldInfo.annotation, fieldInfo.field.getType());
switch (fieldInfo.annotation.type()) {
+ case Bool:
+ value = buf.get() != 0;
+ break;
case U8:
value = (short) (buf.get() & 0xFF);
break;
@@ -457,6 +465,9 @@
private static void putFieldValue(final ByteBuffer output, final FieldInfo fieldInfo,
final Object value) throws BufferUnderflowException {
switch (fieldInfo.annotation.type()) {
+ case Bool:
+ output.put((byte) (value != null && (boolean) value ? 1 : 0));
+ break;
case U8:
output.put((byte) (((short) value) & 0xFF));
break;
@@ -748,6 +759,16 @@
return sb.toString();
}
+ /** A simple Struct which only contains a bool field. */
+ public static class Bool extends Struct {
+ @Struct.Field(order = 0, type = Struct.Type.Bool)
+ public final boolean val;
+
+ public Bool(final boolean val) {
+ this.val = val;
+ }
+ }
+
/** A simple Struct which only contains a u8 field. */
public static class U8 extends Struct {
@Struct.Field(order = 0, type = Struct.Type.U8)
diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
similarity index 95%
rename from Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
rename to staticlibs/device/com/android/net/module/util/SyncStateMachine.java
index a17eb26..fc0161b 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
+++ b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.networkstack.tethering.util;
+package com.android.net.module.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -165,6 +165,11 @@
* The message is processed sequentially, so calling this method recursively is not permitted.
* In other words, using this method inside State#enter, State#exit, or State#processMessage
* is incorrect and will result in an IllegalStateException.
+ *
+ * @param what is assigned to Message.what
+ * @param arg1 is assigned to Message.arg1
+ * @param arg2 is assigned to Message.arg2
+ * @param obj is assigned to Message.obj
*/
public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
ensureCorrectThread();
@@ -189,6 +194,15 @@
mCurrentlyProcessing = Integer.MIN_VALUE;
}
+ /**
+ * Synchronously process a message and perform state transition.
+ *
+ * @param what is assigned to Message.what.
+ */
+ public final void processMessage(int what) {
+ processMessage(what, 0, 0, null);
+ }
+
private void maybeProcessSelfMessageQueue() {
while (!mSelfMsgQueue.isEmpty()) {
currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
@@ -211,7 +225,8 @@
consideredState = mStateInfo.get(consideredState.parent);
}
if (null == consideredState) {
- Log.wtf(mName, "Message " + msg.what + " was not handled");
+ final String state = mCurrentState == null ? "null" : mCurrentState.getName();
+ Log.wtf(mName, "Message " + msg.what + " was not handled. Current state: " + state);
}
performTransitions();
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 541a375..f34159e 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,7 @@
import static android.system.OsConstants.SO_RCVTIMEO;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
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;
@@ -57,6 +58,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
@@ -225,6 +227,96 @@
}
/**
+ * Sends an RTM_NEWLINK message to kernel to set a network interface up or down.
+ *
+ * @param ifName The name of the network interface to modify.
+ * @param isUp {@code true} to set the interface up, {@code false} to set it down.
+ * @return {@code true} if the request was successfully sent, {@code false} otherwise.
+ */
+ public static boolean sendRtmSetLinkStateRequest(@NonNull String ifName, boolean isUp) {
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ ifName, 1 /*sequenceNumber*/, isUp);
+ if (msg == null) {
+ return false;
+ }
+
+ final byte[] bytes = msg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, bytes);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Fail to set the interface " + ifName + " " + (isUp ? "up" : "down"), e);
+ return false;
+ }
+ }
+
+ /**
+ * Sends an RTM_NEWLINK message to kernel to rename a network interface.
+ *
+ * @param ifName The current name of the network interface.
+ * @param newIfName The new name to assign to the interface.
+ * @return {@code true} if the request was successfully sent, {@code false} otherwise.
+ */
+ public static boolean sendRtmSetLinkNameRequest(
+ @NonNull String ifName, @NonNull String newIfName) {
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkNameMessage(
+ ifName, 1 /*sequenceNumber*/, newIfName);
+ if (msg == null) {
+ return false;
+ }
+
+ final byte[] bytes = msg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, bytes);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Fail to rename the interface from " + ifName + " to " + newIfName, e);
+ return false;
+ }
+ }
+
+ /**
+ * Gets the information of a network interface using a Netlink message.
+ * <p>
+ * This method sends a Netlink message to the kernel to request information about the specified
+ * network interface and returns a {@link RtNetlinkLinkMessage} containing the interface status.
+ *
+ * @param ifName The name of the network interface to query.
+ * @return An {@link RtNetlinkLinkMessage} containing the interface status, or {@code null} if
+ * the interface does not exist or an error occurred during the query.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage getLinkRequest(@NonNull String ifName) {
+ final int ifIndex = new OsAccess().if_nametoindex(ifName);
+ if (ifIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ final AtomicReference<RtNetlinkLinkMessage> recvMsg = new AtomicReference<>();
+ final Consumer<RtNetlinkLinkMessage> handleNlMsg = (msg) -> {
+ if (msg.getHeader().nlmsg_type == RTM_NEWLINK
+ && msg.getIfinfoHeader().index == ifIndex) {
+ recvMsg.set(msg);
+ }
+ };
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createGetLinkMessage(
+ ifName, 1 /*sequenceNumber*/);
+ if (msg == null) {
+ return null;
+ }
+
+ final byte[] bytes = msg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.getAndProcessNetlinkDumpMessages(
+ bytes, NETLINK_ROUTE, RtNetlinkLinkMessage.class, handleNlMsg);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ // Nothing we can do here.
+ }
+ return recvMsg.get();
+ }
+
+ /**
* Create netlink socket with the given netlink protocol type and buffersize.
*
* @param nlProto the netlink protocol
diff --git a/staticlibs/device/com/android/net/module/util/netlink/OsAccess.java b/staticlibs/device/com/android/net/module/util/netlink/OsAccess.java
new file mode 100644
index 0000000..7591d5c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/OsAccess.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 com.android.net.module.util.netlink;
+
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This class wraps the static methods of {@link android.system.Os} for mocking and testing.
+ */
+public class OsAccess {
+ /**
+ * Constant indicating that the {@code if_nametoindex()} function could not find the network
+ * interface index corresponding to the given interface name.
+ */
+ public static int INVALID_INTERFACE_INDEX = 0;
+
+ /** Wraps {@link Os#if_nametoindex(String)}. */
+ public int if_nametoindex(@NonNull String name) {
+ return Os.if_nametoindex(name);
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index df7010e..037d95f 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -16,6 +16,15 @@
package com.android.net.module.util.netlink;
+import static android.system.OsConstants.AF_UNSPEC;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETLINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST_ACK;
+
import android.net.MacAddress;
import android.system.OsConstants;
@@ -24,6 +33,7 @@
import androidx.annotation.VisibleForTesting;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
/**
* A NetlinkMessage subclass for rtnetlink link messages.
@@ -46,20 +56,57 @@
public static final short IN6_ADDR_GEN_MODE_NONE = 1;
- private int mMtu;
- @NonNull
- private StructIfinfoMsg mIfinfomsg;
- @Nullable
- private MacAddress mHardwareAddress;
- @Nullable
- private String mInterfaceName;
+ // The maximum buffer size to hold an interface name including the null-terminator '\0'.
+ private static final int IFNAMSIZ = 16;
+ // The default value of MTU, which means the MTU is unspecified.
+ private static final int DEFAULT_MTU = 0;
- private RtNetlinkLinkMessage(@NonNull StructNlMsgHdr header) {
- super(header);
- mIfinfomsg = null;
- mMtu = 0;
- mHardwareAddress = null;
- mInterfaceName = null;
+ @NonNull
+ private final StructIfinfoMsg mIfinfomsg;
+ private final int mMtu;
+ @Nullable
+ private final MacAddress mHardwareAddress;
+ @Nullable
+ private final String mInterfaceName;
+
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance.
+ *
+ * <p>This method validates the arguments and returns {@code null} if any of them are invalid.
+ * nlmsghdr's nlmsg_len will be updated to the correct length before creation.
+ *
+ * @param nlmsghdr The Netlink message header. Must not be {@code null}.
+ * @param ifinfomsg The interface information message. Must not be {@code null}.
+ * @param mtu The Maximum Transmission Unit (MTU) value for the link.
+ * @param hardwareAddress The hardware address (MAC address) of the link. May be {@code null}.
+ * @param interfaceName The name of the interface. May be {@code null}.
+ * @return A new {@link RtNetlinkLinkMessage} instance, or {@code null} if the input arguments
+ * are invalid.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage build(@NonNull StructNlMsgHdr nlmsghdr,
+ @NonNull StructIfinfoMsg ifinfomsg, int mtu, @Nullable MacAddress hardwareAddress,
+ @Nullable String interfaceName) {
+ if (mtu < 0) {
+ return null;
+ }
+ if (interfaceName != null
+ && (interfaceName.isEmpty() || interfaceName.length() + 1 > IFNAMSIZ)) {
+ return null;
+ }
+
+ nlmsghdr.nlmsg_len = calculateMessageLength(mtu, hardwareAddress, interfaceName);
+ return new RtNetlinkLinkMessage(nlmsghdr, ifinfomsg, mtu, hardwareAddress, interfaceName);
+ }
+
+ private RtNetlinkLinkMessage(@NonNull StructNlMsgHdr nlmsghdr,
+ @NonNull StructIfinfoMsg ifinfomsg, int mtu, @Nullable MacAddress hardwareAddress,
+ @Nullable String interfaceName) {
+ super(nlmsghdr);
+ mIfinfomsg = ifinfomsg;
+ mMtu = mtu;
+ mHardwareAddress = hardwareAddress;
+ mInterfaceName = interfaceName;
}
public int getMtu() {
@@ -91,33 +138,46 @@
@Nullable
public static RtNetlinkLinkMessage parse(@NonNull final StructNlMsgHdr header,
@NonNull final ByteBuffer byteBuffer) {
- final RtNetlinkLinkMessage linkMsg = new RtNetlinkLinkMessage(header);
-
- linkMsg.mIfinfomsg = StructIfinfoMsg.parse(byteBuffer);
- if (linkMsg.mIfinfomsg == null) return null;
+ final StructIfinfoMsg ifinfoMsg = StructIfinfoMsg.parse(byteBuffer);
+ if (ifinfoMsg == null) {
+ return null;
+ }
// IFLA_MTU
+ int mtu = DEFAULT_MTU;
final int baseOffset = byteBuffer.position();
StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(IFLA_MTU, byteBuffer);
if (nlAttr != null) {
- linkMsg.mMtu = nlAttr.getValueAsInt(0 /* default value */);
+ mtu = nlAttr.getValueAsInt(DEFAULT_MTU);
}
// IFLA_ADDRESS
+ MacAddress hardwareAddress = null;
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(IFLA_ADDRESS, byteBuffer);
if (nlAttr != null) {
- linkMsg.mHardwareAddress = nlAttr.getValueAsMacAddress();
+ hardwareAddress = nlAttr.getValueAsMacAddress();
}
// IFLA_IFNAME
+ String interfaceName = null;
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(IFLA_IFNAME, byteBuffer);
if (nlAttr != null) {
- linkMsg.mInterfaceName = nlAttr.getValueAsString();
+ interfaceName = nlAttr.getValueAsString();
}
- return linkMsg;
+ return new RtNetlinkLinkMessage(header, ifinfoMsg, mtu, hardwareAddress, interfaceName);
+ }
+
+ /**
+ * Write a rtnetlink link message to {@link byte} array.
+ */
+ public byte[] pack(ByteOrder order) {
+ byte[] bytes = new byte[mHeader.nlmsg_len];
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(order);
+ pack(buffer);
+ return bytes;
}
/**
@@ -125,10 +185,10 @@
*/
@VisibleForTesting
protected void pack(ByteBuffer byteBuffer) {
- getHeader().pack(byteBuffer);
+ mHeader.pack(byteBuffer);
mIfinfomsg.pack(byteBuffer);
- if (mMtu != 0) {
+ if (mMtu != DEFAULT_MTU) {
final StructNlAttr mtu = new StructNlAttr(IFLA_MTU, mMtu);
mtu.pack(byteBuffer);
}
@@ -142,11 +202,121 @@
}
}
+ /**
+ * Calculate the byte length of the packed buffer.
+ */
+ private static int calculateMessageLength(int mtu, MacAddress hardwareAddress,
+ String interfaceName) {
+ int length = StructNlMsgHdr.STRUCT_SIZE + StructIfinfoMsg.STRUCT_SIZE;
+
+ if (mtu != DEFAULT_MTU) {
+ length += NetlinkConstants.alignedLengthOf(StructNlAttr.NLA_HEADERLEN + Integer.BYTES);
+ }
+ if (hardwareAddress != null) {
+ length += NetlinkConstants.alignedLengthOf(
+ StructNlAttr.NLA_HEADERLEN + ETHER_ADDR_LEN);
+ }
+ if (interfaceName != null) {
+ length += NetlinkConstants.alignedLengthOf(
+ // The string should be end with '\0', so the length should plus 1.
+ StructNlAttr.NLA_HEADERLEN + interfaceName.length() + 1);
+ }
+
+ return length;
+ }
+
+ /**
+ * Create a link message to set the operational state (up or down) of a network interface.
+ *
+ * @param interfaceName The network interface name.
+ * @param sequenceNumber The sequence number to use for the Netlink message.
+ * @param isUp {@code true} to set the interface up, {@code false} to set it down.
+ * @return A `RtNetlinkLinkMessage` instance configured to set the link state.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetLinkStateMessage(@NonNull String interfaceName,
+ int sequenceNumber, boolean isUp) {
+ return createSetLinkStateMessage(interfaceName, sequenceNumber, isUp, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetLinkStateMessage(@NonNull String interfaceName,
+ int sequenceNumber, boolean isUp, OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex,
+ isUp ? IFF_UP : 0, IFF_UP), DEFAULT_MTU, null, null);
+ }
+
+ /**
+ * Create a link message to rename the network interface.
+ *
+ * @param interfaceName The network interface name.
+ * @param sequenceNumber The sequence number to use for the Netlink message.
+ * @param newName The new name of the network interface.
+ * @return A `RtNetlinkLinkMessage` instance configured to rename the network interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetLinkNameMessage(@NonNull String interfaceName,
+ int sequenceNumber, @NonNull String newName) {
+ return createSetLinkNameMessage(interfaceName, sequenceNumber, newName, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetLinkNameMessage(@NonNull String interfaceName,
+ int sequenceNumber, @NonNull String newName, OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex, 0, 0),
+ DEFAULT_MTU, null, newName);
+ }
+
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to get the link information
+ * of a network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createGetLinkMessage(@NonNull String interfaceName,
+ int sequenceNumber) {
+ return createGetLinkMessage(interfaceName, sequenceNumber, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createGetLinkMessage(@NonNull String interfaceName,
+ int sequenceNumber, @NonNull OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(0, RTM_GETLINK, NLM_F_REQUEST, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex, 0, 0),
+ DEFAULT_MTU, null, null);
+ }
+
@Override
public String toString() {
return "RtNetlinkLinkMessage{ "
+ "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
- + "Ifinfomsg{" + mIfinfomsg.toString() + "}, "
+ + "Ifinfomsg{" + mIfinfomsg + "}, "
+ "Hardware Address{" + mHardwareAddress + "}, "
+ "MTU{" + mMtu + "}, "
+ "Ifname{" + mInterfaceName + "} "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index 5272366..7cc95de 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -32,10 +32,11 @@
// Already aligned.
public static final int STRUCT_SIZE = 16;
- public static final short NLM_F_REQUEST = 0x0001;
- public static final short NLM_F_MULTI = 0x0002;
- public static final short NLM_F_ACK = 0x0004;
- public static final short NLM_F_ECHO = 0x0008;
+ public static final short NLM_F_REQUEST = 0x0001;
+ public static final short NLM_F_MULTI = 0x0002;
+ public static final short NLM_F_ACK = 0x0004;
+ public static final short NLM_F_ECHO = 0x0008;
+ public static final short NLM_F_REQUEST_ACK = NLM_F_REQUEST | NLM_F_ACK;
// Flags for a GET request.
public static final short NLM_F_ROOT = 0x0100;
public static final short NLM_F_MATCH = 0x0200;
diff --git a/staticlibs/framework/com/android/net/module/util/DnsUtils.java b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
new file mode 100644
index 0000000..19ffd72
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Dns common utility functions.
+ *
+ * @hide
+ */
+public class DnsUtils {
+
+ private DnsUtils() { }
+
+ /**
+ * Convert the string to DNS case-insensitive uppercase.
+ *
+ * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
+ * their unaccented counterparts. So the "DNS uppercase" should be if character is a-z then they
+ * transform into A-Z. Otherwise, they are kept as-is.
+ */
+ public static String toDnsUpperCase(@NonNull String string) {
+ final char[] outChars = new char[string.length()];
+ for (int i = 0; i < string.length(); i++) {
+ outChars[i] = toDnsUpperCase(string.charAt(i));
+ }
+ return new String(outChars);
+ }
+
+ /**
+ * Convert the array of labels to DNS case-insensitive uppercase.
+ */
+ public static String[] toDnsLabelsUpperCase(@NonNull String[] labels) {
+ final String[] outStrings = new String[labels.length];
+ for (int i = 0; i < labels.length; ++i) {
+ outStrings[i] = toDnsUpperCase(labels[i]);
+ }
+ return outStrings;
+ }
+
+ /**
+ * Compare two strings by DNS case-insensitive uppercase.
+ */
+ 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 (toDnsUpperCase(a.charAt(i)) != toDnsUpperCase(b.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compare two set of DNS labels by DNS case-insensitive uppercase.
+ */
+ public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
+ if (a == b) {
+ return true;
+ }
+ int length = a.length;
+ if (b.length != length) {
+ return false;
+ }
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreDnsCase(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static char toDnsUpperCase(char a) {
+ return a >= 'a' && a <= 'z' ? (char) (a - ('a' - 'A')) : a;
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index f6bee69..e4d25cd 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -16,6 +16,8 @@
package com.android.net.module.util;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.Nullable;
@@ -112,48 +114,14 @@
*
* @return {@link LocationPermissionCheckStatus} the result of the location permission check.
*/
- public @LocationPermissionCheckStatus int checkLocationPermissionWithDetailInfo(
+ @VisibleForTesting(visibility = PRIVATE)
+ public @LocationPermissionCheckStatus int checkLocationPermissionInternal(
String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
- final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
- switch (result) {
- case ERROR_LOCATION_MODE_OFF:
- Log.e(TAG, "Location mode is disabled for the device");
- break;
- case ERROR_LOCATION_PERMISSION_MISSING:
- Log.e(TAG, "UID " + uid + " has no location permission");
- break;
+ try {
+ checkPackage(uid, pkgName);
+ } catch (SecurityException e) {
+ return ERROR_LOCATION_PERMISSION_MISSING;
}
- return result;
- }
-
- /**
- * Enforce the caller has location permission.
- *
- * This API determines if the location mode enabled for the caller and the caller has
- * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
- * SecurityException is thrown if the caller has no permission or the location mode is disabled.
- *
- * @param pkgName package name of the application requesting access
- * @param featureId The feature in the package
- * @param uid The uid of the package
- * @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 void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
- @Nullable String message) throws SecurityException {
- final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
-
- switch (result) {
- case ERROR_LOCATION_MODE_OFF:
- throw new SecurityException("Location mode is disabled for the device");
- case ERROR_LOCATION_PERMISSION_MISSING:
- throw new SecurityException("UID " + uid + " has no location permission");
- }
- }
-
- private int checkLocationPermissionInternal(String pkgName, @Nullable String featureId,
- int uid, @Nullable String message) {
- checkPackage(uid, pkgName);
// Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
// are granted a bypass.
@@ -221,7 +189,7 @@
/**
* Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
*/
- public boolean isLocationModeEnabled() {
+ private boolean isLocationModeEnabled() {
final LocationManager LocationManager = mContext.getSystemService(LocationManager.class);
try {
return LocationManager.isLocationEnabledForUser(UserHandle.of(
@@ -278,7 +246,7 @@
/**
* Returns true if the |uid| holds NETWORK_SETTINGS permission.
*/
- public boolean checkNetworkSettingsPermission(int uid) {
+ private boolean checkNetworkSettingsPermission(int uid) {
return getUidPermission(android.Manifest.permission.NETWORK_SETTINGS, uid)
== PackageManager.PERMISSION_GRANTED;
}
@@ -286,7 +254,7 @@
/**
* Returns true if the |uid| holds NETWORK_SETUP_WIZARD permission.
*/
- public boolean checkNetworkSetupWizardPermission(int uid) {
+ private boolean checkNetworkSetupWizardPermission(int uid) {
return getUidPermission(android.Manifest.permission.NETWORK_SETUP_WIZARD, uid)
== PackageManager.PERMISSION_GRANTED;
}
@@ -294,7 +262,7 @@
/**
* Returns true if the |uid| holds NETWORK_STACK permission.
*/
- public boolean checkNetworkStackPermission(int uid) {
+ private boolean checkNetworkStackPermission(int uid) {
return getUidPermission(android.Manifest.permission.NETWORK_STACK, uid)
== PackageManager.PERMISSION_GRANTED;
}
@@ -302,7 +270,7 @@
/**
* Returns true if the |uid| holds MAINLINE_NETWORK_STACK permission.
*/
- public boolean checkMainlineNetworkStackPermission(int uid) {
+ private boolean checkMainlineNetworkStackPermission(int uid) {
return getUidPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, uid)
== PackageManager.PERMISSION_GRANTED;
}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 319d51a..f1ff2e4 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -129,7 +129,9 @@
public static final int IPV6_PROTOCOL_OFFSET = 6;
public static final int IPV6_SRC_ADDR_OFFSET = 8;
public static final int IPV6_DST_ADDR_OFFSET = 24;
+ public static final int IPV6_FRAGMENT_ID_OFFSET = 4;
public static final int IPV6_MIN_MTU = 1280;
+ public static final int IPV6_FRAGMENT_ID_LEN = 4;
public static final int IPV6_FRAGMENT_HEADER_LEN = 8;
public static final int RFC7421_PREFIX_LENGTH = 64;
// getSockOpt() for v6 MTU
@@ -141,6 +143,8 @@
public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
+ public static final int IPPROTO_FRAGMENT = 44;
+
/**
* ICMP constants.
*
@@ -256,6 +260,18 @@
public static final short DNS_OVER_TLS_PORT = 853;
/**
+ * Dns query type constants.
+ *
+ * See also:
+ * - https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2
+ */
+ public static final int TYPE_A = 1;
+ public static final int TYPE_PTR = 12;
+ public static final int TYPE_TXT = 16;
+ public static final int TYPE_AAAA = 28;
+ public static final int TYPE_SRV = 33;
+
+ /**
* IEEE802.11 standard constants.
*
* See also:
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
index 41a9428..28ff770 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -16,23 +16,12 @@
package com.android.net.module.util;
-import android.app.usage.NetworkStats;
-
-import com.android.internal.annotations.VisibleForTesting;
-
/**
* Various utilities used for NetworkStats related code.
*
* @hide
*/
public class NetworkStatsUtils {
- // These constants must be synced with the definition in android.net.NetworkStats.
- // TODO: update to formal APIs once all downstreams have these APIs.
- private static final int SET_ALL = -1;
- private static final int METERED_ALL = -1;
- private static final int ROAMING_ALL = -1;
- private static final int DEFAULT_NETWORK_ALL = -1;
-
/**
* Safely multiple a value by a rational.
* <p>
@@ -99,77 +88,4 @@
if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
return amount < low ? low : (amount > high ? high : amount);
}
-
- /**
- * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
- */
- public static android.net.NetworkStats fromPublicNetworkStats(
- NetworkStats publiceNetworkStats) {
- android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
- while (publiceNetworkStats.hasNextBucket()) {
- NetworkStats.Bucket bucket = new NetworkStats.Bucket();
- publiceNetworkStats.getNextBucket(bucket);
- final android.net.NetworkStats.Entry entry = fromBucket(bucket);
- stats = stats.addEntry(entry);
- }
- return stats;
- }
-
- @VisibleForTesting
- public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
- return new android.net.NetworkStats.Entry(
- null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
- convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
- convertBucketRoaming(bucket.getRoaming()),
- convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
- bucket.getRxBytes(), bucket.getRxPackets(),
- bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
- }
-
- private static int convertBucketState(int networkStatsSet) {
- switch (networkStatsSet) {
- case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
- case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
- case NetworkStats.Bucket.STATE_FOREGROUND:
- return android.net.NetworkStats.SET_FOREGROUND;
- }
- return 0;
- }
-
- private static int convertBucketTag(int tag) {
- switch (tag) {
- case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
- }
- return tag;
- }
-
- private static int convertBucketMetered(int metered) {
- switch (metered) {
- case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
- case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
- case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
- }
- return 0;
- }
-
- private static int convertBucketRoaming(int roaming) {
- switch (roaming) {
- case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
- case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
- case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
- }
- return 0;
- }
-
- private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
- switch (defaultNetworkStatus) {
- case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
- return DEFAULT_NETWORK_ALL;
- case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
- return android.net.NetworkStats.DEFAULT_NETWORK_NO;
- case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
- return android.net.NetworkStats.DEFAULT_NETWORK_YES;
- }
- return 0;
- }
}
diff --git a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
index 463b0c4..98d91a5 100644
--- a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
+++ b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
@@ -87,7 +87,9 @@
}
}
- @VisibleForTesting
+ /**
+ * Get the current counter value for the given uid.
+ */
public synchronized int get(int uid) {
return mUidToCount.get(uid, 0);
}
diff --git a/staticlibs/lint-baseline.xml b/staticlibs/lint-baseline.xml
deleted file mode 100644
index 2ee3a43..0000000
--- a/staticlibs/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?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/README.md b/staticlibs/native/README.md
index 1f505c4..7e0e963 100644
--- a/staticlibs/native/README.md
+++ b/staticlibs/native/README.md
@@ -27,4 +27,4 @@
library (`.so`) file, and different versions of the library loaded in the same process by
different modules will in general have different versions. It's important that each of these
libraries loads the common function from its own library. Static linkage should guarantee this
- because static linkage resolves symbols at build time, not runtime.
\ No newline at end of file
+ because static linkage resolves symbols at build time, not runtime.
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index d266cbc..2de5ed7 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -203,7 +203,7 @@
void record(Level lvl, const std::string& entry);
mutable std::shared_mutex mLock;
- std::deque<const std::string> mEntries; // GUARDED_BY(mLock), when supported
+ std::deque<std::string> mEntries; // GUARDED_BY(mLock), when supported
};
} // namespace netdutils
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index fa466f8..8c54e6a 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -27,10 +27,11 @@
"net-utils-device-common-ip",
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
+ "net-utils-service-connectivity",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
],
visibility: [
"//frameworks/base/packages/Tethering/tests/integration",
@@ -39,6 +40,7 @@
"//packages/modules/NetworkStack/tests/integration",
],
lint: {
+ strict_updatability_linting: true,
test: true,
},
}
@@ -56,4 +58,28 @@
],
jarjar_rules: "jarjar-rules.txt",
test_suites: ["device-tests"],
+ lint: {
+ strict_updatability_linting: true,
+ },
+}
+
+python_test_host {
+ name: "NetworkStaticLibHostPythonTests",
+ srcs: [
+ "host/python/*.py",
+ ],
+ main: "host/python/run_tests.py",
+ libs: [
+ "absl-py",
+ "mobly",
+ "net-tests-utils-host-python-common",
+ ],
+ test_config: "host/python/test_config.xml",
+ test_suites: [
+ "general-tests",
+ ],
+ // MoblyBinaryHostTest doesn't support unit_test.
+ test_options: {
+ unit_test: false,
+ },
}
diff --git a/staticlibs/tests/unit/host/python/adb_utils_test.py b/staticlibs/tests/unit/host/python/adb_utils_test.py
new file mode 100644
index 0000000..8fcca37
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/adb_utils_test.py
@@ -0,0 +1,122 @@
+# 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.
+
+from unittest.mock import MagicMock, patch
+from absl.testing import parameterized
+from mobly import asserts
+from mobly import base_test
+from mobly import config_parser
+from net_tests_utils.host.python import adb_utils
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
+
+
+class TestAdbUtils(base_test.BaseTestClass, parameterized.TestCase):
+
+ def __init__(self, configs: config_parser.TestRunConfig):
+ super().__init__(configs)
+
+ def setup_test(self):
+ self.mock_ad = MagicMock() # Mock Android device object
+ self.mock_ad.log = MagicMock()
+ self.mock_ad.adb.shell.return_value = b"" # Default empty return for shell
+
+ @patch(
+ "net_tests_utils.host.python.adb_utils.expect_dumpsys_state_with_retry"
+ )
+ @patch("net_tests_utils.host.python.adb_utils._set_screen_state")
+ def test_set_doze_mode_enable(
+ self, mock_set_screen_state, mock_expect_dumpsys_state
+ ):
+ adb_utils.set_doze_mode(self.mock_ad, True)
+ mock_set_screen_state.assert_called_once_with(self.mock_ad, False)
+
+ @patch(
+ "net_tests_utils.host.python.adb_utils.expect_dumpsys_state_with_retry"
+ )
+ def test_set_doze_mode_disable(self, mock_expect_dumpsys_state):
+ adb_utils.set_doze_mode(self.mock_ad, False)
+
+ @patch("net_tests_utils.host.python.adb_utils._get_screen_state")
+ def test_set_screen_state_success(self, mock_get_screen_state):
+ mock_get_screen_state.side_effect = [False, True] # Simulate toggle
+ adb_utils._set_screen_state(self.mock_ad, True)
+
+ @patch("net_tests_utils.host.python.adb_utils._get_screen_state")
+ def test_set_screen_state_failure(self, mock_get_screen_state):
+ mock_get_screen_state.return_value = False # State doesn't change
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ adb_utils._set_screen_state(self.mock_ad, True)
+
+ @parameterized.parameters(
+ ("Awake", True),
+ ("Asleep", False),
+ ("Dozing", False),
+ ("SomeOtherState", False),
+ ) # Declare inputs for state_str and expected_result.
+ @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+ def test_get_screen_state(self, state_str, expected_result, mock_get_value):
+ mock_get_value.return_value = state_str
+ asserts.assert_equal(
+ adb_utils._get_screen_state(self.mock_ad), expected_result
+ )
+
+ def test_get_value_of_key_from_dumpsys(self):
+ self.mock_ad.adb.shell.return_value = (
+ b"mWakefulness=Awake\nmOtherKey=SomeValue"
+ )
+ result = adb_utils.get_value_of_key_from_dumpsys(
+ self.mock_ad, "power", "mWakefulness"
+ )
+ asserts.assert_equal(result, "Awake")
+
+ @parameterized.parameters(
+ (True, ["true"]),
+ (False, ["false"]),
+ (
+ True,
+ ["false", "true"],
+ ), # Expect True, get False which is unexpected, then get True
+ (
+ False,
+ ["true", "false"],
+ ), # Expect False, get True which is unexpected, then get False
+ ) # Declare inputs for expected_state and returned_value
+ @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+ def test_expect_dumpsys_state_with_retry_success(
+ self, expected_state, returned_value, mock_get_value
+ ):
+ mock_get_value.side_effect = returned_value
+ # Verify the method returns and does not throw.
+ adb_utils.expect_dumpsys_state_with_retry(
+ self.mock_ad, "service", "key", expected_state, 0
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+ def test_expect_dumpsys_state_with_retry_failure(self, mock_get_value):
+ mock_get_value.return_value = "false"
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ adb_utils.expect_dumpsys_state_with_retry(
+ self.mock_ad, "service", "key", True, 0
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+ def test_expect_dumpsys_state_with_retry_not_found(self, mock_get_value):
+ # Simulate the get_value_of_key_from_dumpsys cannot find the give key.
+ mock_get_value.return_value = None
+
+ # Expect the function to raise UnexpectedBehaviorError due to the exception
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ adb_utils.expect_dumpsys_state_with_retry(
+ self.mock_ad, "service", "key", True
+ )
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
new file mode 100644
index 0000000..2885460
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -0,0 +1,229 @@
+# 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.
+
+from unittest.mock import MagicMock, patch
+from absl.testing import parameterized
+from mobly import asserts
+from mobly import base_test
+from mobly import config_parser
+from mobly.controllers.android_device_lib.adb import AdbError
+from net_tests_utils.host.python.apf_utils import (
+ ApfCapabilities,
+ PatternNotFoundException,
+ UnsupportedOperationException,
+ get_apf_capabilities,
+ get_apf_counter,
+ get_apf_counters_from_dumpsys,
+ get_ipv4_addresses,
+ get_ipv6_addresses,
+ get_hardware_address,
+ is_send_raw_packet_downstream_supported,
+ send_raw_packet_downstream,
+)
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
+
+TEST_IFACE_NAME = "eth0"
+TEST_PACKET_IN_HEX = "AABBCCDDEEFF"
+
+
+class TestApfUtils(base_test.BaseTestClass, parameterized.TestCase):
+
+ def __init__(self, configs: config_parser.TestRunConfig):
+ super().__init__(configs)
+
+ def setup_test(self):
+ self.mock_ad = MagicMock() # Mock Android device object
+
+ @patch("net_tests_utils.host.python.adb_utils.get_dumpsys_for_service")
+ def test_get_apf_counters_from_dumpsys_success(
+ self, mock_get_dumpsys: MagicMock
+ ) -> None:
+ mock_get_dumpsys.return_value = """
+IpClient.wlan0
+ APF packet counters:
+ COUNTER_NAME1: 123
+ COUNTER_NAME2: 456
+"""
+ counters = get_apf_counters_from_dumpsys(self.mock_ad, "wlan0")
+ asserts.assert_equal(counters, {"COUNTER_NAME1": 123, "COUNTER_NAME2": 456})
+
+ @patch("net_tests_utils.host.python.adb_utils.get_dumpsys_for_service")
+ def test_get_apf_counters_from_dumpsys_exceptions(
+ self, mock_get_dumpsys: MagicMock
+ ) -> None:
+ test_cases = [
+ "",
+ "IpClient.wlan0\n",
+ "IpClient.wlan0\n APF packet counters:\n",
+ """
+IpClient.wlan1
+ APF packet counters:
+ COUNTER_NAME1: 123
+ COUNTER_NAME2: 456
+""",
+ ]
+
+ for dumpsys_output in test_cases:
+ mock_get_dumpsys.return_value = dumpsys_output
+ with asserts.assert_raises(PatternNotFoundException):
+ get_apf_counters_from_dumpsys(self.mock_ad, "wlan0")
+
+ @patch("net_tests_utils.host.python.apf_utils.get_apf_counters_from_dumpsys")
+ def test_get_apf_counter(self, mock_get_counters: MagicMock) -> None:
+ iface = "wlan0"
+ mock_get_counters.return_value = {
+ "COUNTER_NAME1": 123,
+ "COUNTER_NAME2": 456,
+ }
+ asserts.assert_equal(
+ get_apf_counter(self.mock_ad, iface, "COUNTER_NAME1"), 123
+ )
+ # Not found
+ asserts.assert_equal(
+ get_apf_counter(self.mock_ad, iface, "COUNTER_NAME3"), 0
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_hardware_address_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = """
+46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
+ link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+"""
+ mac_address = get_hardware_address(self.mock_ad, "wlan0")
+ asserts.assert_equal(mac_address, "72:05:77:82:21:E0")
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_hardware_address_not_found(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = "Some output without MAC address"
+ with asserts.assert_raises(PatternNotFoundException):
+ get_hardware_address(self.mock_ad, "wlan0")
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv4_addresses_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = """
+54: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
+ inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan0
+ valid_lft forever preferred_lft forever
+ inet 192.168.200.1/24 brd 192.168.200.255 scope global wlan0
+ valid_lft forever preferred_lft forever
+"""
+ ip_addresses = get_ipv4_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses, ["192.168.195.162", "192.168.200.1"])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv4_addresses_not_found(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ""
+ ip_addresses = get_ipv4_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses, [])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv6_addresses_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = """
+54: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
+ inet6 fe80::10a3:5dff:fe52:de32/64 scope link
+ valid_lft forever preferred_lft forever
+ inet6 2001:b400:e53f:164e:9c1e:780e:d1:4658/64 scope global dynamic mngtmpaddr noprefixroute
+ valid_lft 6995sec preferred_lft 6995sec
+ inet6 fe80::3aff:2199:2d8e:20d1/64 scope link noprefixroute
+ valid_lft forever preferred_lft forever
+"""
+ ip_addresses = get_ipv6_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses,
+ ["fe80::10a3:5dff:fe52:de32",
+ "2001:b400:e53f:164e:9c1e:780e:d1:4658",
+ "fe80::3aff:2199:2d8e:20d1"])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv6_address_not_found(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ""
+ ip_addresses = get_ipv6_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses, [])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_send_raw_packet_downstream_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = "" # Successful command output
+ send_raw_packet_downstream(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ mock_adb_shell.assert_called_once_with(
+ self.mock_ad,
+ "cmd network_stack send-raw-packet-downstream"
+ f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_send_raw_packet_downstream_failure(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ( # Unexpected command output
+ "Any Unexpected Output"
+ )
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ send_raw_packet_downstream(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ asserts.assert_true(
+ is_send_raw_packet_downstream_supported(self.mock_ad),
+ "Send raw packet should be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_send_raw_packet_downstream_unsupported(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.side_effect = AdbError(
+ cmd="", stdout="Unknown command", stderr="", ret_code=3
+ )
+ with asserts.assert_raises(UnsupportedOperationException):
+ send_raw_packet_downstream(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ asserts.assert_false(
+ is_send_raw_packet_downstream_supported(self.mock_ad),
+ "Send raw packet should not be supported.",
+ )
+
+ @parameterized.parameters(
+ ("2,2048,1", ApfCapabilities(2, 2048, 1)), # Valid input
+ ("3,1024,0", ApfCapabilities(3, 1024, 0)), # Valid input
+ ("invalid,output", ApfCapabilities(0, 0, 0)), # Invalid input
+ ("", ApfCapabilities(0, 0, 0)), # Empty input
+ )
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_apf_capabilities(
+ self, mock_output, expected_result, mock_adb_shell
+ ):
+ """Tests the get_apf_capabilities function with various inputs and expected results."""
+ # Configure the mock adb_shell to return the specified output
+ mock_adb_shell.return_value = mock_output
+
+ # Call the function under test
+ result = get_apf_capabilities(self.mock_ad, "wlan0")
+
+ # Assert that the result matches the expected result
+ asserts.assert_equal(result, expected_result)
diff --git a/staticlibs/tests/unit/host/python/assert_utils_test.py b/staticlibs/tests/unit/host/python/assert_utils_test.py
new file mode 100644
index 0000000..7a33373
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/assert_utils_test.py
@@ -0,0 +1,94 @@
+# 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.
+
+from mobly import asserts
+from mobly import base_test
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError, expect_with_retry
+
+
+class TestAssertUtils(base_test.BaseTestClass):
+
+ def test_predicate_succeed(self):
+ """Test when the predicate becomes True within retries."""
+ call_count = 0
+
+ def predicate():
+ nonlocal call_count
+ call_count += 1
+ return call_count > 2 # True on the third call
+
+ expect_with_retry(predicate, max_retries=5, retry_interval_sec=0)
+ asserts.assert_equal(call_count, 3) # Ensure it was called exactly 3 times
+
+ def test_predicate_failed(self):
+ """Test when the predicate never becomes True."""
+
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ expect_with_retry(
+ predicate=lambda: False, max_retries=3, retry_interval_sec=0
+ )
+
+ def test_retry_action_not_called_succeed(self):
+ """Test that the retry_action is not called if the predicate returns true in the first try."""
+ retry_action_called = False
+
+ def retry_action():
+ nonlocal retry_action_called
+ retry_action_called = True
+
+ expect_with_retry(
+ predicate=lambda: True,
+ retry_action=retry_action,
+ max_retries=5,
+ retry_interval_sec=0,
+ )
+ asserts.assert_false(
+ retry_action_called, "retry_action called."
+ ) # Assert retry_action was NOT called
+
+ def test_retry_action_not_called_failed(self):
+ """Test that the retry_action is not called if the max_retries is reached."""
+ retry_action_called = False
+
+ def retry_action():
+ nonlocal retry_action_called
+ retry_action_called = True
+
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ expect_with_retry(
+ predicate=lambda: False,
+ retry_action=retry_action,
+ max_retries=1,
+ retry_interval_sec=0,
+ )
+ asserts.assert_false(
+ retry_action_called, "retry_action called."
+ ) # Assert retry_action was NOT called
+
+ def test_retry_action_called(self):
+ """Test that the retry_action is executed when provided."""
+ retry_action_called = False
+
+ def retry_action():
+ nonlocal retry_action_called
+ retry_action_called = True
+
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ expect_with_retry(
+ predicate=lambda: False,
+ retry_action=retry_action,
+ max_retries=2,
+ retry_interval_sec=0,
+ )
+ asserts.assert_true(retry_action_called, "retry_action not called.")
diff --git a/staticlibs/tests/unit/host/python/packet_utils_test.py b/staticlibs/tests/unit/host/python/packet_utils_test.py
new file mode 100644
index 0000000..8ad9576
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/packet_utils_test.py
@@ -0,0 +1,72 @@
+# 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.
+
+from mobly import asserts
+from mobly import base_test
+from net_tests_utils.host.python import packet_utils
+
+class TestPacketUtils(base_test.BaseTestClass):
+ def test_unicast_arp_request(self):
+ # Using scapy to generate unicast arp request packet:
+ # eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+ # arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
+ # pkt = eth/arp
+ expect_arp_request = """
+ 01020304050600010203040508060001080006040001000102030405c0a80102000000000000c0a80101
+ """.upper().replace(" ", "").replace("\n", "")
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac="00:01:02:03:04:05",
+ dst_mac="01:02:03:04:05:06",
+ src_ip="192.168.1.2",
+ dst_ip="192.168.1.1",
+ op=packet_utils.ARP_REQUEST_OP
+ )
+ asserts.assert_equal(expect_arp_request, arp_request)
+
+ def test_broadcast_arp_request(self):
+ # Using scapy to generate unicast arp request packet:
+ # eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
+ # arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
+ # pkt = eth/arp
+ expect_arp_request = """
+ ffffffffffff00010203040508060001080006040001000102030405c0a80102000000000000c0a80101
+ """.upper().replace(" ", "").replace("\n", "")
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac="00:01:02:03:04:05",
+ dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
+ src_ip="192.168.1.2",
+ dst_ip="192.168.1.1",
+ op=packet_utils.ARP_REQUEST_OP
+ )
+ asserts.assert_equal(expect_arp_request, arp_request)
+
+ def test_arp_reply(self):
+ # Using scapy to generate unicast arp request packet:
+ # eth = Ether(src="01:02:03:04:05:06", dst="00:01:02:03:04:05")
+ # arp = ARP(op=2, pdst="192.168.1.2", \
+ # hwsrc="01:02:03:04:05:06", \
+ # psrc="192.168.1.1", \
+ # hwdst="00:01:02:03:04:05")
+ # pkt = eth/arp
+ expect_arp_reply = """
+ 00010203040501020304050608060001080006040002010203040506c0a80101000102030405c0a80102
+ """.upper().replace(" ", "").replace("\n", "")
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac="01:02:03:04:05:06",
+ dst_mac="00:01:02:03:04:05",
+ src_ip="192.168.1.1",
+ dst_ip="192.168.1.2",
+ op=packet_utils.ARP_REPLY_OP
+ )
+ asserts.assert_equal(expect_arp_reply, arp_reply)
diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py
new file mode 100644
index 0000000..498dbaf
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/run_tests.py
@@ -0,0 +1,36 @@
+# 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.
+
+"""Main entrypoint for all of unittest."""
+
+import sys
+from host.python.adb_utils_test import TestAdbUtils
+from host.python.apf_utils_test import TestApfUtils
+from host.python.assert_utils_test import TestAssertUtils
+from host.python.packet_utils_test import TestPacketUtils
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+ # For MoblyBinaryHostTest, this entry point will be called twice:
+ # 1. List tests.
+ # <mobly-par-file-name> -- --list_tests
+ # 2. Run tests.
+ # <mobly-par-file-name> -- --config=<yaml-path> --device_serial=<device-serial> --log_path=<log-path>
+ # Strip the "--" since suite runner doesn't recognize it.
+ sys.argv.pop(1)
+ # TODO: make the tests can be executed without manually list classes.
+ suite_runner.run_suite(
+ [TestAssertUtils, TestAdbUtils, TestApfUtils, TestPacketUtils], sys.argv
+ )
diff --git a/staticlibs/tests/unit/host/python/test_config.xml b/staticlibs/tests/unit/host/python/test_config.xml
new file mode 100644
index 0000000..fed9d11
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/test_config.xml
@@ -0,0 +1,21 @@
+<?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 NetworkStaticLibHostPythonTests">
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest" >
+ <option name="mobly-par-file-name" value="NetworkStaticLibHostPythonTests" />
+ <option name="mobly-test-timeout" value="3m" />
+ </test>
+</configuration>
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 9fb61d9..a5af09b 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
@@ -24,6 +24,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.DNS_RESOLVER_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
import static org.junit.Assert.assertEquals;
@@ -77,7 +78,9 @@
private static final int TEST_DEFAULT_FLAG_VALUE = 0;
private static final int TEST_MAX_FLAG_VALUE = 1000;
private static final int TEST_MIN_FLAG_VALUE = 100;
- private static final long TEST_PACKAGE_VERSION = 290000000;
+ private static final long TEST_PACKAGE_VERSION = 290500000;
+ private static final long TEST_GO_PACKAGE_VERSION = 290000000; // Not updated
+ private static final long TEST_RESOLV_PACKAGE_VERSION = 290300000; // Updated, but older.
private static final String TEST_PACKAGE_NAME = "test.package.name";
// The APEX name is the name of the APEX module, as in android.content.pm.ModuleInfo, and is
// used for its mount point in /apex. APEX packages are actually APKs with a different
@@ -85,14 +88,18 @@
// that manifest, and is reflected in android.content.pm.ApplicationInfo. Contrary to the APEX
// (module) name, different package names are typically used to identify the organization that
// built and signed the APEX modules.
- private static final String TEST_APEX_PACKAGE_NAME = "com.prefix.android.tethering";
- private static final String TEST_GO_APEX_PACKAGE_NAME = "com.prefix.android.go.tethering";
+ private static final String TEST_TETHERING_PACKAGE_NAME = "com.prefix.android.tethering";
+ private static final String TEST_GO_TETHERING_PACKAGE_NAME = "com.prefix.android.go.tethering";
+ private static final String TEST_RESOLV_PACKAGE_NAME = "com.prefix.android.resolv";
+ private static final String TEST_GO_RESOLV_PACKAGE_NAME = "com.prefix.android.go.resolv";
private static final String TEST_CONNRES_PACKAGE_NAME =
"com.prefix.android.connectivity.resources";
private static final String TEST_NETWORKSTACK_NAME = "com.prefix.android.networkstack";
private static final String TEST_GO_NETWORKSTACK_NAME = "com.prefix.android.go.networkstack";
private final PackageInfo mPackageInfo = new PackageInfo();
- private final PackageInfo mApexPackageInfo = new PackageInfo();
+ private final PackageInfo mGoApexPackageInfo = new PackageInfo();
+ private final PackageInfo mTetheringApexPackageInfo = new PackageInfo();
+ private final PackageInfo mResolvApexPackageInfo = new PackageInfo();
private MockitoSession mSession;
@Mock private Context mContext;
@@ -105,13 +112,22 @@
mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
mPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
- mApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+ mTetheringApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+ mGoApexPackageInfo.setLongVersionCode(TEST_GO_PACKAGE_VERSION);
+ mResolvApexPackageInfo.setLongVersionCode(TEST_RESOLV_PACKAGE_VERSION);
doReturn(mPm).when(mContext).getPackageManager();
doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
doReturn(mPackageInfo).when(mPm).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
- doReturn(mApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_APEX_PACKAGE_NAME), anyInt());
+ doReturn(mTetheringApexPackageInfo).when(mPm).getPackageInfo(
+ eq(TEST_TETHERING_PACKAGE_NAME), anyInt());
+ doReturn(mResolvApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_RESOLV_PACKAGE_NAME),
+ anyInt());
+ doReturn(mGoApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_GO_TETHERING_PACKAGE_NAME),
+ anyInt());
+ doReturn(mGoApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_GO_RESOLV_PACKAGE_NAME),
+ anyInt());
doReturn(mResources).when(mContext).getResources();
@@ -342,9 +358,9 @@
@Test
public void testFeatureIsEnabledOnGo() throws Exception {
doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(
- eq(TEST_APEX_PACKAGE_NAME), anyInt());
- doReturn(mApexPackageInfo).when(mPm).getPackageInfo(
- eq(TEST_GO_APEX_PACKAGE_NAME), anyInt());
+ eq(TEST_TETHERING_PACKAGE_NAME), anyInt());
+ doReturn(mTetheringApexPackageInfo).when(mPm).getPackageInfo(
+ eq(TEST_GO_TETHERING_PACKAGE_NAME), anyInt());
doReturn("0").when(() -> DeviceConfig.getProperty(
NAMESPACE_CONNECTIVITY, TEST_EXPERIMENT_FLAG));
doReturn("0").when(() -> DeviceConfig.getProperty(
@@ -483,6 +499,31 @@
mContext, 889900000L + CONNECTIVITY_MODULE_ID));
}
+
+ @Test
+ public void testIsFeatureSupported_resolvFeature() throws Exception {
+ assertTrue(DeviceConfigUtils.isFeatureSupported(
+ mContext, TEST_RESOLV_PACKAGE_VERSION + DNS_RESOLVER_MODULE_ID));
+ // Return false because feature requires a future version.
+ assertFalse(DeviceConfigUtils.isFeatureSupported(
+ mContext, 889900000L + DNS_RESOLVER_MODULE_ID));
+ }
+
+ @Test
+ public void testIsFeatureSupported_goResolvFeature() throws Exception {
+ doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(eq(TEST_RESOLV_PACKAGE_NAME),
+ anyInt());
+ doReturn(mGoApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_GO_RESOLV_PACKAGE_NAME),
+ anyInt());
+ assertFalse(DeviceConfigUtils.isFeatureSupported(
+ mContext, TEST_RESOLV_PACKAGE_VERSION + DNS_RESOLVER_MODULE_ID));
+ assertTrue(DeviceConfigUtils.isFeatureSupported(
+ mContext, TEST_GO_PACKAGE_VERSION + DNS_RESOLVER_MODULE_ID));
+ // Return false because feature requires a future version.
+ assertFalse(DeviceConfigUtils.isFeatureSupported(
+ mContext, 889900000L + DNS_RESOLVER_MODULE_ID));
+ }
+
@Test
public void testIsFeatureSupported_illegalModule() throws Exception {
assertThrows(IllegalArgumentException.class,
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
new file mode 100644
index 0000000..7b1f08a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.toDnsLabelsUpperCase
+import com.android.net.module.util.DnsUtils.toDnsUpperCase
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+class DnsUtilsTest {
+ @Test
+ fun testToDnsUpperCase() {
+ assertEquals("TEST", toDnsUpperCase("TEST"))
+ assertEquals("TEST", toDnsUpperCase("TeSt"))
+ assertEquals("TEST", toDnsUpperCase("test"))
+ assertEquals("TÉST", toDnsUpperCase("TÉST"))
+ assertEquals("ţéST", toDnsUpperCase("ţést"))
+ // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+ // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+ assertEquals(
+ "TEST: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+ toDnsUpperCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
+ )
+ // Also test some characters where the first surrogate is not \ud800
+ assertEquals(
+ "TEST: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+ toDnsUpperCase(
+ "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+ )
+ )
+ }
+
+ @Test
+ fun testToDnsLabelsUpperCase() {
+ assertArrayEquals(
+ arrayOf("TEST", "TÉST", "ţéST"),
+ toDnsLabelsUpperCase(arrayOf("TeSt", "TÉST", "ţést"))
+ )
+ }
+
+ @Test
+ fun testEqualsIgnoreDnsCase() {
+ assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
+ assertTrue(equalsIgnoreDnsCase("TEST", "test"))
+ assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
+ assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
+ assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
+ // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+ // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+ assertTrue(equalsIgnoreDnsCase(
+ "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+ "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
+ ))
+ // Also test some characters where the first surrogate is not \ud800
+ assertTrue(equalsIgnoreDnsCase(
+ "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+ "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+ ))
+ }
+
+ @Test
+ fun testEqualsLabelIgnoreDnsCase() {
+ assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
new file mode 100644
index 0000000..4b740e3
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GrowingIntArrayTest {
+ @Test
+ fun testAddAndGet() {
+ val array = GrowingIntArray(1)
+ array.add(-1)
+ array.add(0)
+ array.add(2)
+
+ assertEquals(-1, array.get(0))
+ assertEquals(0, array.get(1))
+ assertEquals(2, array.get(2))
+ assertEquals(3, array.length())
+ }
+
+ @Test
+ fun testForEach() {
+ val array = GrowingIntArray(10)
+ array.add(-1)
+ array.add(0)
+ array.add(2)
+
+ val actual = mutableListOf<Int>()
+ array.forEach { actual.add(it) }
+
+ val expected = listOf(-1, 0, 2)
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun testForEach_EmptyArray() {
+ val array = GrowingIntArray(10)
+ array.forEach {
+ fail("This should not be called")
+ }
+ }
+
+ @Test
+ fun testRemoveValues() {
+ val array = GrowingIntArray(10)
+ array.add(-1)
+ array.add(0)
+ array.add(2)
+
+ array.removeValues { it <= 0 }
+ assertEquals(1, array.length())
+ assertEquals(2, array.get(0))
+ }
+
+ @Test
+ fun testContains() {
+ val array = GrowingIntArray(10)
+ array.add(-1)
+ array.add(2)
+
+ assertTrue(array.contains(-1))
+ assertTrue(array.contains(2))
+
+ assertFalse(array.contains(0))
+ assertFalse(array.contains(3))
+ }
+
+ @Test
+ fun testClear() {
+ val array = GrowingIntArray(10)
+ array.add(-1)
+ array.add(2)
+ array.clear()
+
+ assertEquals(0, array.length())
+ }
+
+ @Test
+ fun testEnsureHasCapacity() {
+ val array = GrowingIntArray(0)
+ array.add(42)
+ array.ensureHasCapacity(2)
+
+ assertEquals(3, array.backingArrayLength)
+ }
+
+ @Test
+ fun testGetMinimizedBackingArray() {
+ val array = GrowingIntArray(10)
+ array.add(-1)
+ array.add(2)
+
+ assertContentEquals(intArrayOf(-1, 2), array.minimizedBackingArray)
+ }
+
+ @Test
+ fun testToString() {
+ assertEquals("[]", GrowingIntArray(10).toString())
+ assertEquals("[1,2,3]", GrowingIntArray(3).apply {
+ add(1)
+ add(2)
+ add(3)
+ }.toString())
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
index 84018a5..d773374 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
@@ -18,17 +18,17 @@
import static android.Manifest.permission.NETWORK_SETTINGS;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
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.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.AppOpsManager;
@@ -46,7 +46,6 @@
import com.android.testutils.DevSdkIgnoreRule;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -106,17 +105,18 @@
}
private void setupMocks() throws Exception {
- when(mMockPkgMgr.getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any()))
- .thenReturn(mMockApplInfo);
- when(mMockContext.getPackageManager()).thenReturn(mMockPkgMgr);
- when(mMockAppOps.noteOp(AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
- TEST_FEATURE_ID, null)).thenReturn(mWifiScanAllowApps);
- when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
- eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
- .thenReturn(mAllowCoarseLocationApps);
- when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
- eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
- .thenReturn(mAllowFineLocationApps);
+ doReturn(mMockApplInfo).when(mMockPkgMgr)
+ .getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any());
+ doReturn(mMockPkgMgr).when(mMockContext).getPackageManager();
+ doReturn(mWifiScanAllowApps).when(mMockAppOps).noteOp(
+ AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
+ TEST_FEATURE_ID, null);
+ doReturn(mAllowCoarseLocationApps).when(mMockAppOps).noteOp(
+ eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
+ eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class));
+ doReturn(mAllowFineLocationApps).when(mMockAppOps).noteOp(
+ eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
+ eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class));
if (mThrowSecurityException) {
doThrow(new SecurityException("Package " + TEST_PKG_NAME + " doesn't belong"
+ " to application bound to user " + mUid))
@@ -128,10 +128,10 @@
}
private <T> void mockSystemService(String name, Class<T> clazz, T service) {
- when(mMockContext.getSystemService(name)).thenReturn(service);
- when(mMockContext.getSystemServiceName(clazz)).thenReturn(name);
+ doReturn(service).when(mMockContext).getSystemService(name);
+ doReturn(name).when(mMockContext).getSystemServiceName(clazz);
// Do not use mockito extended final method mocking
- when(mMockContext.getSystemService(clazz)).thenCallRealMethod();
+ doCallRealMethod().when(mMockContext).getSystemService(clazz);
}
private void setupTestCase() throws Exception {
@@ -167,16 +167,17 @@
Binder.restoreCallingIdentity((((long) mUid) << 32) | Binder.getCallingPid());
doAnswer(mReturnPermission).when(mMockContext).checkPermission(
anyString(), anyInt(), anyInt());
- when(mMockUserManager.isSameProfileGroup(UserHandle.SYSTEM,
- UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID)))
- .thenReturn(true);
- when(mMockContext.checkPermission(mManifestStringCoarse, -1, mUid))
- .thenReturn(mCoarseLocationPermission);
- when(mMockContext.checkPermission(mManifestStringFine, -1, mUid))
- .thenReturn(mFineLocationPermission);
- when(mMockContext.checkPermission(NETWORK_SETTINGS, -1, mUid))
- .thenReturn(mNetworkSettingsPermission);
- when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(mIsLocationEnabled);
+ doReturn(true).when(mMockUserManager)
+ .isSameProfileGroup(UserHandle.SYSTEM,
+ UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID));
+ doReturn(mCoarseLocationPermission).when(mMockContext)
+ .checkPermission(mManifestStringCoarse, -1, mUid);
+ doReturn(mFineLocationPermission).when(mMockContext)
+ .checkPermission(mManifestStringFine, -1, mUid);
+ doReturn(mNetworkSettingsPermission).when(mMockContext)
+ .checkPermission(NETWORK_SETTINGS, -1, mUid);
+ doReturn(mIsLocationEnabled).when(mLocationManager)
+ .isLocationEnabledForUser(any());
}
private Answer<Integer> createPermissionAnswer() {
@@ -208,7 +209,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
@@ -225,7 +226,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
@@ -239,9 +240,9 @@
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
setupTestCase();
- assertThrows(SecurityException.class,
- () -> mChecker.checkLocationPermissionWithDetailInfo(
- TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
+ final int result = mChecker.checkLocationPermissionInternal(
+ TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+ assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
}
@Test
@@ -251,7 +252,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
}
@@ -267,7 +268,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
verify(mMockAppOps, never()).noteOp(anyInt(), anyInt(), anyString());
@@ -284,7 +285,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.ERROR_LOCATION_MODE_OFF, result);
}
@@ -298,18 +299,8 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
-
-
- private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
- try {
- r.run();
- Assert.fail("Expected " + exceptionClass + " to be thrown.");
- } catch (Exception exception) {
- assertTrue(exceptionClass.isInstance(exception));
- }
- }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index 2785ea9..9981b6a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -16,16 +16,12 @@
package com.android.net.module.util
-import android.net.NetworkStats
-import android.text.TextUtils
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -75,68 +71,4 @@
assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11))
assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11))
}
-
- @Test
- fun testBucketToEntry() {
- val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
- android.app.usage.NetworkStats.Bucket.TAG_NONE,
- android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
- android.app.usage.NetworkStats.Bucket.METERED_YES,
- android.app.usage.NetworkStats.Bucket.ROAMING_NO,
- android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
- val entry = NetworkStatsUtils.fromBucket(bucket)
- val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
- NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
- NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
- 0 /* operations */)
-
- // TODO: Use assertEquals once all downstreams accept null iface in
- // NetworkStats.Entry#equals.
- assertEntryEquals(expectedEntry, entry)
- }
-
- private fun makeMockBucket(
- uid: Int,
- tag: Int,
- state: Int,
- metered: Int,
- roaming: Int,
- defaultNetwork: Int,
- rxBytes: Long,
- rxPackets: Long,
- txBytes: Long,
- txPackets: Long
- ): android.app.usage.NetworkStats.Bucket {
- val ret: android.app.usage.NetworkStats.Bucket =
- mock(android.app.usage.NetworkStats.Bucket::class.java)
- doReturn(uid).`when`(ret).getUid()
- doReturn(tag).`when`(ret).getTag()
- doReturn(state).`when`(ret).getState()
- doReturn(metered).`when`(ret).getMetered()
- doReturn(roaming).`when`(ret).getRoaming()
- doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
- doReturn(rxBytes).`when`(ret).getRxBytes()
- doReturn(rxPackets).`when`(ret).getRxPackets()
- doReturn(txBytes).`when`(ret).getTxBytes()
- doReturn(txPackets).`when`(ret).getTxPackets()
- return ret
- }
-
- /**
- * Assert that the two {@link NetworkStats.Entry} are equals.
- */
- private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
- TextUtils.equals(left.iface, right.iface)
- assertEquals(left.uid, right.uid)
- assertEquals(left.set, right.set)
- assertEquals(left.tag, right.tag)
- assertEquals(left.metered, right.metered)
- assertEquals(left.roaming, right.roaming)
- assertEquals(left.defaultNetwork, right.defaultNetwork)
- assertEquals(left.rxBytes, right.rxBytes)
- assertEquals(left.rxPackets, right.rxPackets)
- assertEquals(left.txBytes, right.txBytes)
- assertEquals(left.txPackets, right.txPackets)
- assertEquals(left.operations, right.operations)
- }
}
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
index e40cd6b..886336c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
@@ -21,9 +21,13 @@
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_FRAGMENT_ID_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_FRAGMENT_ID_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
import static com.android.net.module.util.NetworkStackConstants.TCP_HEADER_MIN_LEN;
import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
@@ -54,6 +58,8 @@
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -489,10 +495,103 @@
(byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
};
+ private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_NO_FRAG =
+ new byte[] {
+ // packet = Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", type='IPv6')/
+ // IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80, fl=0x515ca,
+ // hlim=0x40)/UDP(sport=9876, dport=433)/
+ // Raw([i%256 for i in range(0, 500)]);
+ // Ether header
+ (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+ (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+ (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+ (byte) 0x86, (byte) 0xdd,
+ // IPv6 header
+ (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+ (byte) 0x01, (byte) 0xfc, (byte) 0x11, (byte) 0x40,
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+ // UDP header
+ (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+ (byte) 0x01, (byte) 0xfc, (byte) 0xd3, (byte) 0x9e,
+ // Data
+ // 500 bytes of repeated 0x00~0xff
+ };
+
+ private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG1 =
+ new byte[] {
+ // packet = Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", type='IPv6')/
+ // IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80, fl=0x515ca,
+ // hlim=0x40)/UDP(sport=9876, dport=433)/
+ // Raw([i%256 for i in range(0, 500)]);
+ // packets=fragment6(packet, 400);
+ // Ether header
+ (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+ (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+ (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+ (byte) 0x86, (byte) 0xdd,
+ // IPv6 header
+ (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+ (byte) 0x01, (byte) 0x58, (byte) 0x2c, (byte) 0x40,
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+ // Fragement Header
+ (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ // UDP header
+ (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+ (byte) 0x01, (byte) 0xfc, (byte) 0xd3, (byte) 0x9e,
+ // Data
+ // 328 bytes of repeated 0x00~0xff, start:0x00 end:0x47
+ };
+
+ private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG2 =
+ new byte[] {
+ // packet = Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", type='IPv6')/
+ // IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80, fl=0x515ca,
+ // hlim=0x40)/UDP(sport=9876, dport=433)/
+ // Raw([i%256 for i in range(0, 500)]);
+ // packets=fragment6(packet, 400);
+ // Ether header
+ (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+ (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+ (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+ (byte) 0x86, (byte) 0xdd,
+ // IPv6 header
+ (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+ (byte) 0x00, (byte) 0xb4, (byte) 0x2c, (byte) 0x40,
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+ // Fragement Header
+ (byte) 0x11, (byte) 0x00, (byte) 0x01, (byte) 0x50,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ // Data
+ // 172 bytes of repeated 0x00~0xff, start:0x48 end:0xf3
+ };
+
/**
* Build a packet which has ether header, IP header, TCP/UDP header and data.
* The ethernet header and data are optional. Note that both source mac address and
- * destination mac address are required for ethernet header.
+ * destination mac address are required for ethernet header. The packet will be fragmented into
+ * multiple smaller packets if the packet size exceeds L2 mtu.
*
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Layer 2 header (EthernetHeader) | (optional)
@@ -511,11 +610,12 @@
* @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
* currently supported.
* @param payload the payload.
+ * @param l2mtu the Link MTU. It's the upper bound of each packet size. Zero means no limit.
*/
@NonNull
- private ByteBuffer buildPacket(@Nullable final MacAddress srcMac,
+ private List<ByteBuffer> buildPackets(@Nullable final MacAddress srcMac,
@Nullable final MacAddress dstMac, final int l3proto, final int l4proto,
- @Nullable final ByteBuffer payload)
+ @Nullable final ByteBuffer payload, int l2mtu)
throws Exception {
if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
fail("Unsupported layer 3 protocol " + l3proto);
@@ -562,7 +662,15 @@
payload.clear();
}
- return packetBuilder.finalizePacket();
+ return packetBuilder.finalizePacket(l2mtu > 0 ? l2mtu : Integer.MAX_VALUE);
+ }
+
+ @NonNull
+ private ByteBuffer buildPacket(@Nullable final MacAddress srcMac,
+ @Nullable final MacAddress dstMac, final int l3proto, final int l4proto,
+ @Nullable final ByteBuffer payload)
+ throws Exception {
+ return buildPackets(srcMac, dstMac, l3proto, l4proto, payload, 0).get(0);
}
/**
@@ -874,6 +982,66 @@
assertArrayEquals(TEST_PACKET_IPV6HDR_UDPHDR_DATA, packet.array());
}
+ private void checkIpv6PacketIgnoreFragmentId(byte[] expected, byte[] actual) {
+ final int offset = ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_FRAGMENT_ID_OFFSET;
+ assertArrayEquals(Arrays.copyOf(expected, offset), Arrays.copyOf(actual, offset));
+ assertArrayEquals(
+ Arrays.copyOfRange(expected, offset + IPV6_FRAGMENT_ID_LEN, expected.length),
+ Arrays.copyOfRange(actual, offset + IPV6_FRAGMENT_ID_LEN, actual.length));
+ }
+
+ @Test
+ public void testBuildPacketIPv6FragmentUdpData() throws Exception {
+ // A UDP packet with 500 bytes payload will be fragmented into two UDP packets each carrying
+ // 328 and 172 bytes of payload if the Link MTU is 400. Note that only the first packet
+ // contains the original UDP header.
+ final int payloadLen = 500;
+ final int payloadLen1 = 328;
+ final int payloadLen2 = 172;
+ final int l2mtu = 400;
+ final byte[] payload = new byte[payloadLen];
+ // Initialize the payload with repeated values from 0x00 to 0xff.
+ for (int i = 0; i < payload.length; i++) {
+ payload[i] = (byte) (i & 0xff);
+ }
+
+ // Verify original UDP packet.
+ final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+ ByteBuffer.wrap(payload));
+ final int headerLen = TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_NO_FRAG.length;
+ assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_NO_FRAG,
+ Arrays.copyOf(packet.array(), headerLen));
+ assertArrayEquals(payload,
+ Arrays.copyOfRange(packet.array(), headerLen, headerLen + payloadLen));
+
+ // Verify fragments of UDP packet.
+ final List<ByteBuffer> packets = buildPackets(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+ ByteBuffer.wrap(payload), l2mtu);
+ assertEquals(2, packets.size());
+
+ // Verify first fragment.
+ int headerLen1 = TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG1.length;
+ // (1) Compare packet content up to the UDP header, excluding the fragment ID as it's a
+ // random value.
+ checkIpv6PacketIgnoreFragmentId(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG1,
+ Arrays.copyOf(packets.get(0).array(), headerLen1));
+ // (2) Compare UDP payload.
+ assertArrayEquals(Arrays.copyOf(payload, payloadLen1),
+ Arrays.copyOfRange(packets.get(0).array(), headerLen1, headerLen1 + payloadLen1));
+
+ // Verify second fragment (similar to the first one).
+ int headerLen2 = TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG2.length;
+ checkIpv6PacketIgnoreFragmentId(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG2,
+ Arrays.copyOf(packets.get(1).array(), headerLen2));
+ assertArrayEquals(Arrays.copyOfRange(payload, payloadLen1, payloadLen1 + payloadLen2),
+ Arrays.copyOfRange(packets.get(1).array(), headerLen2, headerLen2 + payloadLen2));
+ // Verify that the fragment IDs in the first and second fragments are the same.
+ final int offset = ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_FRAGMENT_ID_OFFSET;
+ assertArrayEquals(
+ Arrays.copyOfRange(packets.get(0).array(), offset, offset + IPV6_FRAGMENT_ID_LEN),
+ Arrays.copyOfRange(packets.get(1).array(), offset, offset + IPV6_FRAGMENT_ID_LEN));
+ }
+
@Test
public void testFinalizePacketWithoutIpv4Header() throws Exception {
final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP,
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
similarity index 97%
rename from tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
index 4e15d5f..b04561c 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.connectivity
+package com.android.net.module.util
import android.net.INetd
import android.os.Build
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index a39b7a3..0c2605f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -32,6 +32,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;
@@ -133,6 +134,29 @@
verifyHeaderParsing(msg);
}
+ @Test
+ public void testBoolStruct() {
+ assertEquals(1, Struct.getSize(Bool.class));
+
+ assertEquals(false, Struct.parse(Bool.class, toByteBuffer("00")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("01")).val);
+ // maybe these should throw instead, but currently only 0 is false...
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("02")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("7F")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("80")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("FF")).val);
+
+ final var f = new Bool(false);
+ final var t = new Bool(true);
+ assertEquals(f.val, false);
+ assertEquals(t.val, true);
+
+ assertArrayEquals(toByteBuffer("00").array(), f.writeToBytes(ByteOrder.BIG_ENDIAN));
+ assertArrayEquals(toByteBuffer("00").array(), f.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+ assertArrayEquals(toByteBuffer("01").array(), t.writeToBytes(ByteOrder.BIG_ENDIAN));
+ assertArrayEquals(toByteBuffer("01").array(), t.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+ }
+
public static class HeaderMsgWithoutConstructor extends Struct {
static int sType;
static int sLength;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
similarity index 98%
rename from Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
index 3a57fdd..d534054 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.networkstack.tethering.util
+package com.android.net.module.util
import android.os.Message
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.util.State
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import com.android.net.module.util.SyncStateMachine.StateInfo
import java.util.ArrayDeque
import java.util.ArrayList
import kotlin.test.assertFailsWith
@@ -45,7 +45,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class SynStateMachineTest {
+class SyncStateMachineTest {
private val mState1 = spy(object : TestState(MSG_1) {})
private val mState2 = spy(object : TestState(MSG_2) {})
private val mState3 = spy(object : TestState(MSG_3) {})
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 9db63db..bd0e31d 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -24,24 +24,28 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
import android.net.MacAddress;
import android.system.OsConstants;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.HexDump;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-@RunWith(AndroidJUnit4.class)
+@RunWith(MockitoJUnitRunner.class)
@SmallTest
public class RtNetlinkLinkMessageTest {
+ @Mock
+ private OsAccess mOsAccess;
// An example of the full RTM_NEWLINK message.
private static final String RTM_NEWLINK_HEX =
@@ -124,14 +128,14 @@
}
private static final String RTM_NEWLINK_PACK_HEX =
- "34000000100000000000000000000000" // struct nlmsghr
+ "40000000100000000000000000000000" // struct nlmsghr
+ "000001001E0000000210000000000000" // struct ifinfo
+ "08000400DC050000" // IFLA_MTU
+ "0A00010092C3E3C9374E0000" // IFLA_ADDRESS
+ "0A000300776C616E30000000"; // IFLA_IFNAME(wlan0)
@Test
- public void testPackRtmNewLink() {
+ public void testParseAndPackRtmNewLink() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_PACK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
@@ -145,6 +149,21 @@
assertEquals(RTM_NEWLINK_PACK_HEX, HexDump.toHexString(packBuffer.array()));
}
+ @Test
+ public void testPackRtmNewLink() {
+ final RtNetlinkLinkMessage linkMsg = RtNetlinkLinkMessage.build(
+ // nlmsg_len will be updated inside create() method, so it's ok to set 0 here.
+ new StructNlMsgHdr(0 /*nlmsg_len*/, (short) 0x10, (short) 0, 0),
+ new StructIfinfoMsg((byte) 0, (short) 1, 0x1e, 0x1002, 0),
+ 1500,
+ MacAddress.fromString("92:c3:e3:c9:37:4e"),
+ "wlan0");
+ assertNotNull(linkMsg);
+
+ final byte[] packBytes = linkMsg.pack(ByteOrder.LITTLE_ENDIAN);
+ assertEquals(RTM_NEWLINK_PACK_HEX, HexDump.toHexString(packBytes));
+ }
+
private static final String RTM_NEWLINK_TRUNCATED_HEX =
"54000000100000000000000000000000" // struct nlmsghr
+ "000001001E0000000210000000000000" // struct ifinfo
@@ -171,6 +190,122 @@
}
@Test
+ public void testCreateSetLinkUpMessage() {
+ final String expectedHexBytes =
+ "20000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000100000001000000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final boolean isUp = true;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ interfaceName, sequenceNumber, isUp, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetLinkDownMessage() {
+ final String expectedHexBytes =
+ "20000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000000000001000000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final boolean isUp = false;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ interfaceName, sequenceNumber, isUp, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetLinkStateMessage_InvalidInterface() {
+ final String interfaceName = "wlan0";
+ final int sequenceNumber = 0x2468;
+ final boolean isUp = false;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(OsAccess.INVALID_INTERFACE_INDEX);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ interfaceName, sequenceNumber, isUp, mOsAccess);
+ assertNull(msg);
+ }
+
+ @Test
+ public void testCreateSetLinkNameMessage() {
+ final String expectedHexBytes =
+ "2C000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000000000000000000" // struct ifinfomsg
+ + "0A000300776C616E31000000"; // IFLA_IFNAME(wlan1)
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final String newName = "wlan1";
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkNameMessage(
+ interfaceName, sequenceNumber, newName, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetLinkNameMessage_InterfaceNotFound() {
+ final String interfaceName = "wlan0";
+ final int sequenceNumber = 0x2468;
+ final String newName = "wlan1";
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(OsAccess.INVALID_INTERFACE_INDEX);
+
+ assertNull(RtNetlinkLinkMessage.createSetLinkNameMessage(
+ interfaceName, sequenceNumber, newName, mOsAccess));
+ }
+
+ @Test
+ public void testCreateSetLinkNameMessage_InvalidNewName() {
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final String[] invalidNames = {"", "interface_name_longer_than_limit"};
+ for (String invalidName : invalidNames) {
+ assertNull(RtNetlinkLinkMessage.createSetLinkNameMessage(
+ interfaceName, sequenceNumber, invalidName, mOsAccess));
+ }
+ }
+
+ @Test
+ public void testCreateGetLinkMessage() {
+ final String expectedHexBytes =
+ "20000000120001006824000000000000" // struct nlmsghdr
+ + "00000000080000000000000000000000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createGetLinkMessage(
+ interfaceName, sequenceNumber, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
public void testToString() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
diff --git a/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
new file mode 100644
index 0000000..7e508fb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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 android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class DefaultNetworkRestoreMonitorTest {
+ private val restoreDefaultNetworkDesc =
+ Description.createSuiteDescription("RestoreDefaultNetwork")
+ private val testDesc = Description.createTestDescription("testClass", "testMethod")
+ private val wifiCap = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ .build()
+ private val cellCap = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ .build()
+ private val cm = mock(ConnectivityManager::class.java)
+ private val pm = mock(PackageManager::class.java).also {
+ doReturn(true).`when`(it).hasSystemFeature(anyString())
+ }
+ private val ctx = mock(Context::class.java).also {
+ doReturn(cm).`when`(it).getSystemService(ConnectivityManager::class.java)
+ doReturn(pm).`when`(it).getPackageManager()
+ }
+ private val notifier = mock(RunNotifier::class.java)
+ private val defaultNetworkMonitor = DefaultNetworkRestoreMonitor(
+ ctx,
+ notifier,
+ timeoutMs = 0
+ )
+
+ private fun getRunListener(): RunListener {
+ val captor = ArgumentCaptor.forClass(RunListener::class.java)
+ verify(notifier).addListener(captor.capture())
+ return captor.value
+ }
+
+ private fun mockDefaultNetworkCapabilities(cap: NetworkCapabilities?) {
+ if (cap == null) {
+ doNothing().`when`(cm).registerDefaultNetworkCallback(any())
+ return
+ }
+ doAnswer {
+ val callback = it.getArgument(0) as NetworkCallback
+ callback.onCapabilitiesChanged(Network(100), cap)
+ }.`when`(cm).registerDefaultNetworkCallback(any())
+ }
+
+ @Test
+ fun testDefaultNetworkRestoreMonitor_defaultNetworkRestored() {
+ mockDefaultNetworkCapabilities(wifiCap)
+ defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+ val listener = getRunListener()
+ listener.testFinished(testDesc)
+
+ defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+ val inOrder = inOrder(notifier)
+ inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+ inOrder.verify(notifier, never()).fireTestFailure(any())
+ inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+ inOrder.verify(notifier).removeListener(listener)
+ }
+
+ @Test
+ fun testDefaultNetworkRestoreMonitor_testStartWithoutDefaultNetwork() {
+ // There is no default network when the tests start
+ mockDefaultNetworkCapabilities(null)
+ defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+ mockDefaultNetworkCapabilities(wifiCap)
+ val listener = getRunListener()
+ listener.testFinished(testDesc)
+
+ defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+ val inOrder = inOrder(notifier)
+ inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+ // fireTestFailure is called
+ inOrder.verify(notifier).fireTestFailure(any())
+ inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+ inOrder.verify(notifier).removeListener(listener)
+ }
+
+ @Test
+ fun testDefaultNetworkRestoreMonitor_testEndWithoutDefaultNetwork() {
+ mockDefaultNetworkCapabilities(wifiCap)
+ defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+ // There is no default network after the test
+ mockDefaultNetworkCapabilities(null)
+ val listener = getRunListener()
+ listener.testFinished(testDesc)
+
+ defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+ val inOrder = inOrder(notifier)
+ inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+ // fireTestFailure is called with method name
+ inOrder.verify(
+ notifier
+ ).fireTestFailure(
+ argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+ )
+ inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+ inOrder.verify(notifier).removeListener(listener)
+ }
+
+ @Test
+ fun testDefaultNetworkRestoreMonitor_testChangeDefaultNetwork() {
+ mockDefaultNetworkCapabilities(wifiCap)
+ defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+ // The default network transport types change after the test
+ mockDefaultNetworkCapabilities(cellCap)
+ val listener = getRunListener()
+ listener.testFinished(testDesc)
+
+ defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+ val inOrder = inOrder(notifier)
+ inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+ // fireTestFailure is called with method name
+ inOrder.verify(
+ notifier
+ ).fireTestFailure(
+ argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+ )
+ inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+ inOrder.verify(notifier).removeListener(listener)
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
new file mode 100644
index 0000000..57920fc
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.NetworkStats
+import android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.TAG_NONE
+import android.os.Build
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val TEST_IFACE = "test0"
+private val TEST_IFACE2: String? = null
+private const val TEST_START = 1194220800000L
+
+@RunWith(JUnit4::class)
+class NetworkStatsUtilsTest {
+ // This is a unit test for a test utility that uses R APIs
+ @Rule @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+ @Test
+ fun testOrderInsensitiveEquals() {
+ val testEntry = arrayOf(
+ NetworkStats.Entry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L),
+ NetworkStats.Entry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+ )
+
+ // Verify equals of empty stats regardless of initial capacity.
+ val red = NetworkStats(TEST_START, 0)
+ val blue = NetworkStats(TEST_START, 1)
+ assertTrue(orderInsensitiveEquals(red, blue))
+ assertTrue(orderInsensitiveEquals(blue, red))
+
+ // Verify not equal.
+ red.combineValues(testEntry[1])
+ blue.combineValues(testEntry[0]).combineValues(testEntry[1])
+ assertFalse(orderInsensitiveEquals(red, blue))
+ assertFalse(orderInsensitiveEquals(blue, red))
+
+ // Verify equals even if the order of entries are not the same.
+ red.combineValues(testEntry[0])
+ assertTrue(orderInsensitiveEquals(red, blue))
+ assertTrue(orderInsensitiveEquals(blue, red))
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 3843b90..8c71a91 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -86,8 +86,8 @@
java_test_host {
name: "net-tests-utils-host-common",
srcs: [
- "host/**/*.java",
- "host/**/*.kt",
+ "host/java/**/*.java",
+ "host/java/**/*.kt",
],
libs: ["tradefed"],
test_suites: [
@@ -104,3 +104,11 @@
],
data: [":ConnectivityTestPreparer"],
}
+
+python_library_host {
+ name: "net-tests-utils-host-python-common",
+ srcs: [
+ "host/python/*.py",
+ ],
+ pkg_path: "net_tests_utils",
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
new file mode 100644
index 0000000..89de0b3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
@@ -0,0 +1,67 @@
+/*
+ * 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 android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class AutoCloseTestInterfaceRule(
+ private val context: Context,
+ ) : TestRule {
+ private val tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+ context.getSystemService(TestNetworkManager::class.java)!!
+ }
+ private val ifaces = ArrayList<TestNetworkInterface>()
+
+ fun createTapInterface(): TestNetworkInterface {
+ return runAsShell(MANAGE_TEST_NETWORKS) {
+ tnm.createTapInterface()
+ }.also {
+ ifaces.add(it)
+ }
+ }
+
+ private fun closeAllInterfaces() {
+ // TODO: wait on RTM_DELLINK before proceeding.
+ for (iface in ifaces) {
+ // ParcelFileDescriptor prevents the fd from being double closed.
+ iface.getFileDescriptor().close()
+ }
+ }
+
+ private inner class AutoCloseTestInterfaceRuleStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ tryTest {
+ base.evaluate()
+ } cleanup {
+ closeAllInterfaces()
+ }
+ }
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return AutoCloseTestInterfaceRuleStatement(base, description)
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
new file mode 100644
index 0000000..dd52d0b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
@@ -0,0 +1,113 @@
+/*
+ * 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 android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.internal.annotations.VisibleForTesting
+import com.android.net.module.util.BitUtils
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.runner.Description
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+class DefaultNetworkRestoreMonitor(
+ ctx: Context,
+ private val notifier: RunNotifier,
+ private val timeoutMs: Long = 30_000
+) {
+ var firstFailure: Exception? = null
+ var initialTransports = 0L
+ val cm = ctx.getSystemService(ConnectivityManager::class.java)!!
+ val pm = ctx.packageManager
+ val listener = object : RunListener() {
+ override fun testFinished(desc: Description) {
+ // Only the first method that does not restore the default network should be blamed.
+ if (firstFailure != null) {
+ return
+ }
+ val cb = TestableNetworkCallback()
+ cm.registerDefaultNetworkCallback(cb)
+ try {
+ cb.eventuallyExpect<RecorderCallback.CallbackEntry.CapabilitiesChanged>(
+ timeoutMs = timeoutMs
+ ) {
+ BitUtils.packBits(it.caps.transportTypes) == initialTransports &&
+ it.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ }
+ } catch (e: AssertionError) {
+ firstFailure = IllegalStateException(desc.methodName + " does not restore the" +
+ "default network, initialTransports = $initialTransports", e)
+ } finally {
+ cm.unregisterNetworkCallback(cb)
+ }
+ }
+ }
+
+ fun init(connectUtil: ConnectUtil) {
+ // Ensure Wi-Fi and cellular connection before running test to avoid starting test
+ // with unexpected default network.
+ // ConnectivityTestTargetPreparer does the same thing, but it's possible that previous tests
+ // don't enable DefaultNetworkRestoreMonitor and the default network is not restored.
+ // This can be removed if all tests enable DefaultNetworkRestoreMonitor
+ if (pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ connectUtil.ensureWifiValidated()
+ }
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ connectUtil.ensureCellularValidated()
+ }
+
+ val capFuture = CompletableFuture<NetworkCapabilities>()
+ val cb = object : ConnectivityManager.NetworkCallback() {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ cap: NetworkCapabilities
+ ) {
+ capFuture.complete(cap)
+ }
+ }
+ cm.registerDefaultNetworkCallback(cb)
+ try {
+ val cap = capFuture.get(10_000, TimeUnit.MILLISECONDS)
+ initialTransports = BitUtils.packBits(cap.transportTypes)
+ } catch (e: Exception) {
+ firstFailure = IllegalStateException(
+ "Failed to get default network status before starting tests", e
+ )
+ } finally {
+ cm.unregisterNetworkCallback(cb)
+ }
+ notifier.addListener(listener)
+ }
+
+ fun reportResultAndCleanUp(desc: Description) {
+ notifier.fireTestStarted(desc)
+ if (firstFailure != null) {
+ notifier.fireTestFailure(
+ Failure(desc, firstFailure)
+ )
+ }
+ notifier.fireTestFinished(desc)
+ notifier.removeListener(listener)
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 69fdbf8..d00ae52 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -16,6 +16,8 @@
package com.android.testutils
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -57,6 +59,12 @@
class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+ private val restoreDefaultNetworkDesc =
+ Description.createTestDescription(klass, "RestoreDefaultNetwork")
+ val ctx = ApplicationProvider.getApplicationContext<Context>()
+ private val restoreDefaultNetwork =
+ klass.isAnnotationPresent(RestoreDefaultNetwork::class.java) &&
+ !ctx.applicationInfo.isInstantApp()
// Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
// Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
@@ -71,6 +79,10 @@
// TODO(b/307693729): Remove this annotation and monitor thread leak by default.
annotation class MonitorThreadLeak
+ // Annotation for test classes to indicate the test runner should verify the default network is
+ // restored after each test.
+ annotation class RestoreDefaultNetwork
+
private val baseRunner: RunnerWrapper<*>? = klass.let {
val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -90,25 +102,10 @@
Modifier.isStatic(it.modifiers) &&
it.isAnnotationPresent(Parameterized.Parameters::class.java) }
- override fun run(notifier: RunNotifier) {
- if (baseRunner == null) {
- // Report a single, skipped placeholder test for this class, as the class is expected to
- // report results when run. In practice runners that apply the Filterable implementation
- // would see a NoTestsRemainException and not call the run method.
- notifier.fireTestIgnored(
- Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
- return
- }
- if (!shouldThreadLeakFailTest) {
- baseRunner.run(notifier)
- return
- }
-
- // Dump threads as a baseline to monitor thread leaks.
- val threadCountsBeforeTest = getAllThreadNameCounts()
-
- baseRunner.run(notifier)
-
+ private fun checkThreadLeak(
+ notifier: RunNotifier,
+ threadCountsBeforeTest: Map<String, Int>
+ ) {
notifier.fireTestStarted(leakMonitorDesc)
val threadCountsAfterTest = getAllThreadNameCounts()
// TODO : move CompareOrUpdateResult to its own util instead of LinkProperties.
@@ -122,13 +119,48 @@
val increasedThreads = threadsDiff.updated
.filter { threadCountsBeforeTest[it.key]!! < it.value }
if (threadsDiff.added.isNotEmpty() || increasedThreads.isNotEmpty()) {
- notifier.fireTestFailure(Failure(leakMonitorDesc,
- IllegalStateException("Unexpected thread changes: $threadsDiff")))
+ notifier.fireTestFailure(Failure(
+ leakMonitorDesc,
+ IllegalStateException("Unexpected thread changes: $threadsDiff")
+ ))
}
+ notifier.fireTestFinished(leakMonitorDesc)
+ }
+
+ override fun run(notifier: RunNotifier) {
+ if (baseRunner == null) {
+ // Report a single, skipped placeholder test for this class, as the class is expected to
+ // report results when run. In practice runners that apply the Filterable implementation
+ // would see a NoTestsRemainException and not call the run method.
+ notifier.fireTestIgnored(
+ Description.createTestDescription(klass, "skippedClassForDevSdkMismatch")
+ )
+ return
+ }
+
+ val networkRestoreMonitor = if (restoreDefaultNetwork) {
+ DefaultNetworkRestoreMonitor(ctx, notifier).apply{
+ init(ConnectUtil(ctx))
+ }
+ } else {
+ null
+ }
+ val threadCountsBeforeTest = if (shouldThreadLeakFailTest) {
+ // Dump threads as a baseline to monitor thread leaks.
+ getAllThreadNameCounts()
+ } else {
+ null
+ }
+
+ baseRunner.run(notifier)
+
+ if (threadCountsBeforeTest != null) {
+ checkThreadLeak(notifier, threadCountsBeforeTest)
+ }
+ networkRestoreMonitor?.reportResultAndCleanUp(restoreDefaultNetworkDesc)
// Clears up internal state of all inline mocks.
// TODO: Call clearInlineMocks() at the end of each test.
Mockito.framework().clearInlineMocks()
- notifier.fireTestFinished(leakMonitorDesc)
}
private fun getAllThreadNameCounts(): Map<String, Int> {
@@ -152,6 +184,9 @@
if (shouldThreadLeakFailTest) {
it.addChild(leakMonitorDesc)
}
+ if (restoreDefaultNetwork) {
+ it.addChild(restoreDefaultNetworkDesc)
+ }
}
}
@@ -162,7 +197,14 @@
// When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
if (baseRunner == null) return 1
- return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
+ var testCount = baseRunner.testCount()
+ if (shouldThreadLeakFailTest) {
+ testCount += 1
+ }
+ if (restoreDefaultNetwork) {
+ testCount += 1
+ }
+ return testCount
}
@Throws(NoTestsRemainException::class)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
new file mode 100644
index 0000000..8608344
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
@@ -0,0 +1,202 @@
+/*
+ * 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 static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static org.junit.Assert.fail;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.net.InetAddresses;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DnsSvcbUtils {
+ private static final Pattern SVC_PARAM_PATTERN = Pattern.compile("([a-z0-9-]+)=?(.*)");
+
+ /**
+ * Returns a DNS SVCB response with given hostname `hostname` and given SVCB records
+ * `records`. Each record must contain the service priority, the target name, and the service
+ * parameters.
+ * E.g. "1 doh.google alpn=h2,h3 port=443 ipv4hint=192.0.2.1 dohpath=/dns-query{?dns}"
+ */
+ @NonNull
+ public static byte[] makeSvcbResponse(String hostname, String[] records) throws IOException {
+ if (records == null) throw new NullPointerException();
+ if (!hostname.startsWith("_dns.")) throw new UnsupportedOperationException();
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ // Write DNS header.
+ os.write(shortsToByteArray(
+ 0x1234, /* Transaction ID */
+ 0x8100, /* Flags */
+ 1, /* qdcount */
+ records.length, /* ancount */
+ 0, /* nscount */
+ 0 /* arcount */
+ ));
+ // Write Question.
+ // - domainNameToLabels() doesn't support the hostname starting with "_", so divide
+ // the writing into two steps.
+ os.write(new byte[] { 0x04, '_', 'd', 'n', 's' });
+ os.write(domainNameToLabels(hostname.substring(5)));
+ os.write(shortsToByteArray(TYPE_SVCB, CLASS_IN));
+ // Write Answer section.
+ for (String r : records) {
+ os.write(makeSvcbRecord(r));
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] makeSvcbRecord(String representation) throws IOException {
+ if (representation == null) return new byte[0];
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(shortsToByteArray(
+ 0xc00c, /* Pointer to qname in question section */
+ TYPE_SVCB,
+ CLASS_IN,
+ 0, 16, /* TTL = 16 */
+ 0 /* Data Length = 0 */
+
+ ));
+ final String[] strings = representation.split(" +");
+ // SvcPriority and TargetName are mandatory in the representation.
+ if (strings.length < 3) {
+ fail("Invalid SVCB representation: " + representation);
+ }
+ // Write SvcPriority, TargetName, and SvcParams.
+ os.write(shortsToByteArray(Short.parseShort(strings[0])));
+ os.write(domainNameToLabels(strings[1]));
+ for (int i = 2; i < strings.length; i++) {
+ try {
+ os.write(svcParamToByteArray(strings[i]));
+ } catch (UnsupportedEncodingException e) {
+ throw new IOException(e);
+ }
+ }
+ // Update rdata length.
+ final byte[] out = os.toByteArray();
+ ByteBuffer.wrap(out).putShort(10, (short) (out.length - 12));
+ return out;
+ }
+
+ @NonNull
+ private static byte[] svcParamToByteArray(String svcParam) throws IOException {
+ final Matcher matcher = SVC_PARAM_PATTERN.matcher(svcParam);
+ if (!matcher.matches() || matcher.groupCount() != 2) {
+ fail("Invalid SvcParam: " + svcParam);
+ }
+ final String svcParamkey = matcher.group(1);
+ final String svcParamValue = matcher.group(2);
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(svcParamKeyToBytes(svcParamkey));
+ switch (svcParamkey) {
+ case "mandatory":
+ final String[] keys = svcParamValue.split(",");
+ os.write(shortsToByteArray(keys.length));
+ for (String v : keys) {
+ os.write(svcParamKeyToBytes(v));
+ }
+ break;
+ case "alpn":
+ os.write(shortsToByteArray((svcParamValue.length() + 1)));
+ for (String v : svcParamValue.split(",")) {
+ os.write(v.length());
+ // TODO: support percent-encoding per RFC 7838.
+ os.write(v.getBytes(US_ASCII));
+ }
+ break;
+ case "no-default-alpn":
+ os.write(shortsToByteArray(0));
+ break;
+ case "port":
+ os.write(shortsToByteArray(2));
+ os.write(shortsToByteArray(Short.parseShort(svcParamValue)));
+ break;
+ case "ipv4hint":
+ final String[] v4Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v4Addrs.length * IPV4_ADDR_LEN)));
+ for (String v : v4Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "ech":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII)); // base64 encoded
+ break;
+ case "ipv6hint":
+ final String[] v6Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v6Addrs.length * IPV6_ADDR_LEN)));
+ for (String v : v6Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "dohpath":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ // TODO: support percent-encoding, since this is a URI template.
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ default:
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] svcParamKeyToBytes(String key) {
+ switch (key) {
+ case "mandatory": return shortsToByteArray(0);
+ case "alpn": return shortsToByteArray(1);
+ case "no-default-alpn": return shortsToByteArray(2);
+ case "port": return shortsToByteArray(3);
+ case "ipv4hint": return shortsToByteArray(4);
+ case "ech": return shortsToByteArray(5);
+ case "ipv6hint": return shortsToByteArray(6);
+ case "dohpath": return shortsToByteArray(7);
+ default:
+ if (!key.startsWith("key")) fail("Invalid SvcParamKey " + key);
+ return shortsToByteArray(Short.parseShort(key.substring(3)));
+ }
+ }
+
+ @NonNull
+ private static byte[] shortsToByteArray(int... values) {
+ final ByteBuffer out = ByteBuffer.allocate(values.length * 2);
+ for (int value: values) {
+ if (value < 0 || value > 0xffff) {
+ throw new AssertionError("not an unsigned short: " + value);
+ }
+ out.putShort((short) value);
+ }
+ return out.array();
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
index 1f82a35..e49c0c7 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
@@ -18,72 +18,56 @@
import android.net.DnsResolver
import android.net.InetAddresses
-import android.os.Looper
+import android.net.Network
import android.os.Handler
+import android.os.Looper
import com.android.internal.annotations.GuardedBy
-import java.net.InetAddress
-import java.util.concurrent.Executor
-import org.mockito.invocation.InvocationOnMock
+import com.android.net.module.util.DnsPacket
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doAnswer
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+import java.net.InetAddress
+import java.net.UnknownHostException
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
-const val TYPE_UNSPECIFIED = -1
-// TODO: Integrate with NetworkMonitorTest.
-class FakeDns(val mockResolver: DnsResolver) {
- class DnsEntry(val hostname: String, val type: Int, val addresses: List<InetAddress>) {
- fun match(host: String, type: Int) = hostname.equals(host) && type == type
- }
+// Nonexistent DNS query type to represent "A and/or AAAA queries".
+// TODO: deduplicate this with DnsUtils.TYPE_ADDRCONFIG.
+private const val TYPE_ADDRCONFIG = -1
- @GuardedBy("answers")
- val answers = ArrayList<DnsEntry>()
+class FakeDns(val network: Network, val dnsResolver: DnsResolver) {
+ private val HANDLER_TIMEOUT_MS = 1000
- fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) {
- return answers.firstOrNull { it.match(hostname, type) }
- }
-
- fun setAnswer(hostname: String, answer: Array<String>, type: Int) = synchronized(answers) {
- val ans = DnsEntry(hostname, type, generateAnswer(answer))
- // Replace or remove the existing one.
- when (val index = answers.indexOfFirst { it.match(hostname, type) }) {
- -1 -> answers.add(ans)
- else -> answers[index] = ans
+ /** Data class to record the Dns entry. */
+ class DnsEntry (val hostname: String, val type: Int, val answerSupplier: AnswerSupplier) {
+ // Full match or partial match that target host contains the entry hostname to support
+ // random private dns probe hostname.
+ fun matches(hostname: String, type: Int): Boolean {
+ return hostname.endsWith(this.hostname) && type == this.type
}
}
- private fun generateAnswer(answer: Array<String>) =
- answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) }
+ /**
+ * Whether queries on [network] will be answered when private DNS is enabled. Queries that
+ * bypass private DNS by using [network.privateDnsBypassingCopy] are always answered.
+ */
+ var nonBypassPrivateDnsWorking: Boolean = true
- fun startMocking() {
- // Mock DnsResolver.query() w/o type
- doAnswer {
- mockAnswer(it, 1, -1, 3, 5)
- }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */,
- any() /* executor */, any() /* cancellationSignal */, any() /*callback*/)
- // Mock DnsResolver.query() w/ type
- doAnswer {
- mockAnswer(it, 1, 2, 4, 6)
- }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */,
- anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */,
- any() /*callback*/)
+ @GuardedBy("answers")
+ private val answers = mutableListOf<DnsEntry>()
+
+ interface AnswerSupplier {
+ /** Supplies the answer to one DnsResolver query method call. */
+ @Throws(DnsResolver.DnsException::class)
+ fun get(): Array<String>?
}
- private fun mockAnswer(
- it: InvocationOnMock,
- posHos: Int,
- posType: Int,
- posExecutor: Int,
- posCallback: Int
- ) {
- val hostname = it.arguments[posHos] as String
- val executor = it.arguments[posExecutor] as Executor
- val callback = it.arguments[posCallback] as DnsResolver.Callback<List<InetAddress>>
- var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED
- val answer = getAnswer(hostname, type)
-
- if (answer != null && !answer.addresses.isNullOrEmpty()) {
- Handler(Looper.getMainLooper()).post({ executor.execute({
- callback.onAnswer(answer.addresses, 0); }) })
+ private class InstantAnswerSupplier(val answers: Array<String>?) : AnswerSupplier {
+ override fun get(): Array<String>? {
+ return answers
}
}
@@ -91,4 +75,177 @@
fun clearAll() = synchronized(answers) {
answers.clear()
}
+
+ /** Returns the answer for a given name and type on the given mock network. */
+ private fun getAnswer(mockNetwork: Network, hostname: String, type: Int):
+ CompletableFuture<Array<String>?> {
+ if (!checkQueryNetwork(mockNetwork)) {
+ return CompletableFuture.completedFuture(null)
+ }
+ val answerSupplier: AnswerSupplier? = synchronized(answers) {
+ answers.firstOrNull({e: DnsEntry -> e.matches(hostname, type)})?.answerSupplier
+ }
+ if (answerSupplier == null) {
+ return CompletableFuture.completedFuture(null)
+ }
+ if (answerSupplier is InstantAnswerSupplier) {
+ // Save latency waiting for a query thread if the answer is hardcoded.
+ return CompletableFuture.completedFuture<Array<String>?>(answerSupplier.get())
+ }
+ val answerFuture = CompletableFuture<Array<String>?>()
+ // Don't worry about ThreadLeadMonitor: these threads terminate immediately, so they won't
+ // leak, and ThreadLeakMonitor won't monitor them anyway, since they have one-time names
+ // such as "Thread-42".
+ Thread {
+ try {
+ answerFuture.complete(answerSupplier.get())
+ } catch (e: DnsResolver.DnsException) {
+ answerFuture.completeExceptionally(e)
+ }
+ }.start()
+ return answerFuture
+ }
+
+ /** Sets the answer for a given name and type. */
+ fun setAnswer(hostname: String, answer: Array<String>?, type: Int) = setAnswer(
+ hostname, InstantAnswerSupplier(answer), type)
+
+ /** Sets the answer for a given name and type. */
+ fun setAnswer(
+ hostname: String, answerSupplier: AnswerSupplier, type: Int) = synchronized (answers) {
+ val ans = DnsEntry(hostname, type, answerSupplier)
+ // Replace or remove the existing one.
+ when (val index = answers.indexOfFirst { it.matches(hostname, type) }) {
+ -1 -> answers.add(ans)
+ else -> answers[index] = ans
+ }
+ }
+
+ private fun checkQueryNetwork(mockNetwork: Network): Boolean {
+ // Queries on the wrong network do not work.
+ // Queries that bypass private DNS work.
+ // Queries that do not bypass private DNS work only if nonBypassPrivateDnsWorking is true.
+ return mockNetwork == network.privateDnsBypassingCopy ||
+ mockNetwork == network && nonBypassPrivateDnsWorking
+ }
+
+ /** Simulates a getAllByName call for the specified name on the specified mock network. */
+ private fun getAllByName(mockNetwork: Network, hostname: String): Array<InetAddress>? {
+ val answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname)
+ .get(HANDLER_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS))
+ if (answer == null || answer.size == 0) {
+ throw UnknownHostException(hostname)
+ }
+ return answer.toTypedArray()
+ }
+
+ // Regardless of the type, depends on what the responses contained in the network.
+ private fun queryAllTypes(
+ mockNetwork: Network, hostname: String
+ ): CompletableFuture<Array<String>?> {
+ val aFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_A)
+ .exceptionally { emptyArray() }
+ val aaaaFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_AAAA)
+ .exceptionally { emptyArray() }
+ val combinedFuture = CompletableFuture<Array<String>?>()
+ aFuture.thenAcceptBoth(aaaaFuture) { res1: Array<String>?, res2: Array<String>? ->
+ var answer: Array<String> = arrayOf()
+ if (res1 != null) answer += res1
+ if (res2 != null) answer += res2
+ combinedFuture.complete(answer)
+ }
+ return combinedFuture
+ }
+
+ /** Starts mocking DNS queries. */
+ fun startMocking() {
+ // Queries on mNetwork using getAllByName.
+ doAnswer {
+ getAllByName(it.mock as Network, it.getArgument(0))
+ }.`when`(network).getAllByName(any())
+
+ // Queries on mCleartextDnsNetwork using DnsResolver#query.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 3, posCallback = 5,
+ posType = -1)
+ }.`when`(dnsResolver).query(any(), any(), anyInt(), any(), any(), any())
+
+ // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 4, posCallback = 6,
+ posType = 2)
+ }.`when`(dnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any())
+
+ // Queries using rawQuery. Currently, mockQuery only supports TYPE_SVCB.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 5, posCallback = 7,
+ posType = 3)
+ }.`when`(dnsResolver).rawQuery(any(), any(), anyInt(), anyInt(), anyInt(), any(), any(),
+ any())
+ }
+
+ private fun stringsToInetAddresses(addrs: Array<String>?): List<InetAddress>? {
+ if (addrs == null) return null
+ val out: MutableList<InetAddress> = ArrayList()
+ for (addr in addrs) {
+ out.add(InetAddresses.parseNumericAddress(addr))
+ }
+ return out
+ }
+
+ // Mocks all the DnsResolver query methods used in this test.
+ private fun mockQuery(
+ invocation: InvocationOnMock, posNetwork: Int, posHostname: Int,
+ posExecutor: Int, posCallback: Int, posType: Int
+ ): Answer<*>? {
+ val hostname = invocation.getArgument<String>(posHostname)
+ val executor = invocation.getArgument<Executor>(posExecutor)
+ val network = invocation.getArgument<Network>(posNetwork)
+ val qtype = if (posType != -1) invocation.getArgument(posType) else TYPE_ADDRCONFIG
+ val answerFuture: CompletableFuture<Array<String>?> = if (posType != -1) getAnswer(
+ network,
+ hostname,
+ invocation.getArgument(posType)
+ ) else queryAllTypes(network, hostname)
+
+ // Discriminate between different callback types to avoid unchecked cast warnings when
+ // calling the onAnswer methods.
+ val inetAddressCallback: DnsResolver.Callback<List<InetAddress>> =
+ invocation.getArgument(posCallback)
+ val byteArrayCallback: DnsResolver.Callback<ByteArray> =
+ invocation.getArgument(posCallback)
+ val callback: DnsResolver.Callback<*> = invocation.getArgument(posCallback)
+
+ answerFuture.whenComplete { answer: Array<String>?, exception: Throwable? ->
+ // Use getMainLooper() because that's what android.net.DnsResolver currently uses.
+ Handler(Looper.getMainLooper()).post {
+ executor.execute {
+ if (exception != null) {
+ if (exception !is DnsResolver.DnsException) {
+ throw java.lang.AssertionError(
+ "Test error building DNS response",
+ exception
+ )
+ }
+ callback.onError((exception as DnsResolver.DnsException?)!!)
+ return@execute
+ }
+ if (answer != null && answer.size > 0) {
+ when (qtype) {
+ DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA, TYPE_ADDRCONFIG ->
+ inetAddressCallback.onAnswer(stringsToInetAddresses(answer)!!, 0)
+ DnsPacket.TYPE_SVCB ->
+ byteArrayCallback.onAnswer(
+ DnsSvcbUtils.makeSvcbResponse(hostname, answer), 0)
+ else -> throw UnsupportedOperationException(
+ "Unsupported qtype $qtype, update this fake"
+ )
+ }
+ }
+ }
+ }
+ }
+ // If the future does not complete or has no answer do nothing. The timeout should fire.
+ return null
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
similarity index 99%
rename from tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
rename to staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
index 93cec9c..8b88224 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.net.cts
+package com.android.testutils
import android.net.DnsResolver
import android.net.Network
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
index 8324b25..26bdb49 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -16,8 +16,21 @@
package com.android.testutils
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.INetworkStatsService
+import android.net.INetworkStatsSession
import android.net.NetworkStats
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.text.TextUtils
+import com.android.modules.utils.build.SdkLevel
import kotlin.test.assertTrue
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
@JvmOverloads
fun orderInsensitiveEquals(
@@ -26,7 +39,7 @@
compareTime: Boolean = false
): Boolean {
if (leftStats == rightStats) return true
- if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+ if (compareTime && leftStats.elapsedRealtime != rightStats.elapsedRealtime) {
return false
}
@@ -47,12 +60,41 @@
left.metered, left.roaming, left.defaultNetwork, i)
if (j == -1) return false
rightTrimmedEmpty.getValues(j, right)
- if (left != right) return false
+ if (SdkLevel.isAtLeastT()) {
+ if (left != right) return false
+ } else {
+ if (!checkEntryEquals(left, right)) return false
+ }
}
return true
}
/**
+ * Assert that the two {@link NetworkStats.Entry} are equals.
+ */
+fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
+ assertTrue(checkEntryEquals(left, right))
+}
+
+// TODO: Make all callers use NetworkStats.Entry#equals once S- downstreams
+// are no longer supported. Because NetworkStats is mainlined on T+ and
+// NetworkStats.Entry#equals in S- does not support null iface.
+fun checkEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry): Boolean {
+ return TextUtils.equals(left.iface, right.iface) &&
+ left.uid == right.uid &&
+ left.set == right.set &&
+ left.tag == right.tag &&
+ left.metered == right.metered &&
+ left.roaming == right.roaming &&
+ left.defaultNetwork == right.defaultNetwork &&
+ left.rxBytes == right.rxBytes &&
+ left.rxPackets == right.rxPackets &&
+ left.txBytes == right.txBytes &&
+ left.txPackets == right.txPackets &&
+ left.operations == right.operations
+}
+
+/**
* Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
* necessarily the same.
*
@@ -66,7 +108,7 @@
compareTime: Boolean = false
) {
assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
- "expected: " + expected + " but was: " + actual)
+ "expected: $expected but was: $actual")
}
/**
@@ -74,5 +116,34 @@
* object.
*/
fun assertParcelingIsLossless(stats: NetworkStats) {
- assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+ assertParcelingIsLossless(stats) { a, b -> orderInsensitiveEquals(a, b) }
+}
+
+/**
+ * Make a {@link android.app.usage.NetworkStats} instance from
+ * a {@link android.net.NetworkStats} instance.
+ */
+// It's not possible to directly create a mocked `NetworkStats` instance
+// because of limitations with `NetworkStats#getNextBucket`.
+// As a workaround for testing, create a mock by controlling the return values
+// from the mocked service that provides the `NetworkStats` data.
+// Notes:
+// 1. The order of records in the final `NetworkStats` object might change or
+// some records might be merged if there are items with duplicate keys.
+// 2. The interface and operations fields will be empty since there is
+// no such field in the {@link android.app.usage.NetworkStats}.
+fun makePublicStatsFromAndroidNetStats(androidNetStats: NetworkStats):
+ android.app.usage.NetworkStats {
+ val mockService = Mockito.mock(INetworkStatsService::class.java)
+ val manager = NetworkStatsManager(Mockito.mock(Context::class.java), mockService)
+ val mockStatsSession = Mockito.mock(INetworkStatsSession::class.java)
+
+ Mockito.doReturn(mockStatsSession).`when`(mockService)
+ .openSessionForUsageStats(anyInt(), any())
+ Mockito.doReturn(androidNetStats).`when`(mockStatsSession).getSummaryForAllUid(
+ any(NetworkTemplate::class.java), anyLong(), anyLong(), anyBoolean())
+ return manager.querySummary(
+ NetworkTemplate.Builder(MATCH_MOBILE).build(),
+ Long.MIN_VALUE, Long.MAX_VALUE
+ )
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
index 70f20d6..58e6622 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -65,10 +65,11 @@
@Override
public void insertEntry(K key, V value) throws ErrnoException,
- IllegalArgumentException {
- // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+ IllegalStateException {
+ // The entry is created if and only if it doesn't exist.
+ // And throws exception if it exists. See BpfMap#insertEntry.
if (mMap.get(key) != null) {
- throw new IllegalArgumentException(key + " already exist");
+ throw new IllegalStateException(key + " already exist");
}
mMap.put(key, value);
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
index 84fb47b..341d55f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
@@ -29,7 +29,6 @@
import android.os.Binder
import android.os.Build
import androidx.annotation.RequiresApi
-import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -137,7 +136,6 @@
network = try {
if (lp != null) {
- assertTrue(isAtLeastR(), "Cannot specify TestNetwork LinkProperties before R")
tnm.setupTestNetwork(lp, true /* isMetered */, binder)
} else {
tnm.setupTestNetwork(iface.interfaceName, binder)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 66362d4..ae43c15 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -44,13 +44,18 @@
object ANY_NETWORK : Network(-2)
fun anyNetwork() = ANY_NETWORK
-open class RecorderCallback private constructor(
- private val backingRecord: ArrayTrackRecord<CallbackEntry>
-) : NetworkCallback() {
- public constructor() : this(ArrayTrackRecord())
- protected constructor(src: RecorderCallback?) : this(src?.backingRecord ?: ArrayTrackRecord())
+private val DEFAULT_TAG = RecorderCallback::class.simpleName
+ ?: fail("Could not determine class name")
- private val TAG = this::class.simpleName
+open class RecorderCallback private constructor(
+ private val backingRecord: ArrayTrackRecord<CallbackEntry>,
+ val logTag: String
+) : NetworkCallback() {
+ public constructor(logTag: String = DEFAULT_TAG) : this(ArrayTrackRecord(), logTag)
+ protected constructor(src: RecorderCallback?, logTag: String) : this(
+ src?.backingRecord ?: ArrayTrackRecord(),
+ logTag
+ )
sealed class CallbackEntry {
// To get equals(), hashcode(), componentN() etc for free, the child classes of
@@ -123,7 +128,7 @@
val mark get() = history.mark
override fun onAvailable(network: Network) {
- Log.d(TAG, "onAvailable $network")
+ Log.d(logTag, "onAvailable $network")
history.add(Available(network))
}
@@ -131,22 +136,22 @@
// expect the callbacks not to record this, do not listen to PreCheck here.
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
- Log.d(TAG, "onCapabilitiesChanged $network $caps")
+ Log.d(logTag, "onCapabilitiesChanged $network $caps")
history.add(CapabilitiesChanged(network, caps))
}
override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
- Log.d(TAG, "onLinkPropertiesChanged $network $lp")
+ Log.d(logTag, "onLinkPropertiesChanged $network $lp")
history.add(LinkPropertiesChanged(network, lp))
}
override fun onLocalNetworkInfoChanged(network: Network, info: LocalNetworkInfo) {
- Log.d(TAG, "onLocalNetworkInfoChanged $network $info")
+ Log.d(logTag, "onLocalNetworkInfoChanged $network $info")
history.add(LocalInfoChanged(network, info))
}
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
- Log.d(TAG, "onBlockedStatusChanged $network $blocked")
+ Log.d(logTag, "onBlockedStatusChanged $network $blocked")
history.add(BlockedStatus(network, blocked))
}
@@ -154,27 +159,27 @@
// fun onBlockedStatusChanged(network: Network, blocked: Int) {
// because on S, that needs to be "override fun", and on R, that cannot be "override fun".
override fun onNetworkSuspended(network: Network) {
- Log.d(TAG, "onNetworkSuspended $network $network")
+ Log.d(logTag, "onNetworkSuspended $network $network")
history.add(Suspended(network))
}
override fun onNetworkResumed(network: Network) {
- Log.d(TAG, "$network onNetworkResumed $network")
+ Log.d(logTag, "$network onNetworkResumed $network")
history.add(Resumed(network))
}
override fun onLosing(network: Network, maxMsToLive: Int) {
- Log.d(TAG, "onLosing $network $maxMsToLive")
+ Log.d(logTag, "onLosing $network $maxMsToLive")
history.add(Losing(network, maxMsToLive))
}
override fun onLost(network: Network) {
- Log.d(TAG, "onLost $network")
+ Log.d(logTag, "onLost $network")
history.add(Lost(network))
}
override fun onUnavailable() {
- Log.d(TAG, "onUnavailable")
+ Log.d(logTag, "onUnavailable")
history.add(Unavailable())
}
}
@@ -188,10 +193,11 @@
*/
open class TestableNetworkCallback private constructor(
src: TestableNetworkCallback?,
- val defaultTimeoutMs: Long = DEFAULT_TIMEOUT,
- val defaultNoCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
- val waiterFunc: Runnable = NOOP // "() -> Unit" would forbid calling with a void func from Java
-) : RecorderCallback(src) {
+ val defaultTimeoutMs: Long,
+ val defaultNoCallbackTimeoutMs: Long,
+ val waiterFunc: Runnable,
+ logTag: String
+) : RecorderCallback(src, logTag) {
/**
* Construct a testable network callback.
* @param timeoutMs the default timeout for expecting a callback. Default 30 seconds. This
@@ -213,14 +219,16 @@
constructor(
timeoutMs: Long = DEFAULT_TIMEOUT,
noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
- waiterFunc: Runnable = NOOP
- ) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc)
+ waiterFunc: Runnable = NOOP, // "() -> Unit" would forbid calling with a void func from Java
+ logTag: String = DEFAULT_TAG
+ ) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc, logTag)
fun createLinkedCopy() = TestableNetworkCallback(
this,
defaultTimeoutMs,
defaultNoCallbackTimeoutMs,
- waiterFunc
+ waiterFunc,
+ logTag
)
// The last available network, or null if any network was lost since the last call to
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
similarity index 100%
rename from staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
rename to staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
diff --git a/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt
similarity index 100%
rename from staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
rename to staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt
diff --git a/staticlibs/testutils/host/python/adb_utils.py b/staticlibs/testutils/host/python/adb_utils.py
new file mode 100644
index 0000000..13c0646
--- /dev/null
+++ b/staticlibs/testutils/host/python/adb_utils.py
@@ -0,0 +1,118 @@
+# 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.
+
+import re
+from mobly.controllers import android_device
+from net_tests_utils.host.python import assert_utils
+
+BYTE_DECODE_UTF_8 = "utf-8"
+
+
+def set_doze_mode(ad: android_device.AndroidDevice, enable: bool) -> None:
+ if enable:
+ adb_shell(ad, "cmd battery unplug")
+ expect_dumpsys_state_with_retry(
+ ad, "deviceidle", key="mCharging", expected_state=False
+ )
+ _set_screen_state(ad, False)
+ adb_shell(ad, "dumpsys deviceidle enable deep")
+ expect_dumpsys_state_with_retry(
+ ad, "deviceidle", key="mDeepEnabled", expected_state=True
+ )
+ adb_shell(ad, "dumpsys deviceidle force-idle deep")
+ expect_dumpsys_state_with_retry(
+ ad, "deviceidle", key="mForceIdle", expected_state=True
+ )
+ else:
+ adb_shell(ad, "cmd battery reset")
+ expect_dumpsys_state_with_retry(
+ ad, "deviceidle", key="mCharging", expected_state=True
+ )
+ adb_shell(ad, "dumpsys deviceidle unforce")
+ expect_dumpsys_state_with_retry(
+ ad, "deviceidle", key="mForceIdle", expected_state=False
+ )
+
+
+def _set_screen_state(
+ ad: android_device.AndroidDevice, target_state: bool
+) -> None:
+ assert_utils.expect_with_retry(
+ predicate=lambda: _get_screen_state(ad) == target_state,
+ retry_action=lambda: adb_shell(
+ ad, "input keyevent KEYCODE_POWER"
+ ), # Toggle power key again when retry.
+ )
+
+
+def _get_screen_state(ad: android_device.AndroidDevice) -> bool:
+ return get_value_of_key_from_dumpsys(ad, "power", "mWakefulness") == "Awake"
+
+
+def get_value_of_key_from_dumpsys(
+ ad: android_device.AndroidDevice, service: str, key: str
+) -> str:
+ output = get_dumpsys_for_service(ad, service)
+ # Search for key=value pattern from the dumpsys output.
+ # e.g. mWakefulness=Awake
+ pattern = rf"{key}=(.*)"
+ # Only look for the first occurrence.
+ match = re.search(pattern, output)
+ if match:
+ ad.log.debug(
+ "Getting key-value from dumpsys: " + key + "=" + match.group(1)
+ )
+ return match.group(1)
+ else:
+ return None
+
+
+def expect_dumpsys_state_with_retry(
+ ad: android_device.AndroidDevice,
+ service: str,
+ key: str,
+ expected_state: bool,
+ retry_interval_sec: int = 1,
+) -> None:
+ def predicate():
+ value = get_value_of_key_from_dumpsys(ad, service, key)
+ if value is None:
+ return False
+ return value.lower() == str(expected_state).lower()
+
+ assert_utils.expect_with_retry(
+ predicate=predicate,
+ retry_interval_sec=retry_interval_sec,
+ )
+
+
+def get_dumpsys_for_service(
+ ad: android_device.AndroidDevice, service: str
+) -> str:
+ return adb_shell(ad, "dumpsys " + service)
+
+
+def adb_shell(ad: android_device.AndroidDevice, shell_cmd: str) -> str:
+ """Runs adb shell command.
+
+ Args:
+ ad: Android device object.
+ shell_cmd: string of list of strings, adb shell command.
+
+ Returns:
+ string, replies from adb shell command.
+ """
+ ad.log.debug("Executing adb shell %s", shell_cmd)
+ data = ad.adb.shell(shell_cmd)
+ return data.decode(BYTE_DECODE_UTF_8).strip()
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
new file mode 100644
index 0000000..9a30978
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -0,0 +1,84 @@
+# 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.
+
+from mobly import asserts
+from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
+
+
+class ApfTestBase(multi_devices_test_base.MultiDevicesTestBase):
+
+ def setup_class(self):
+ super().setup_class()
+
+ # Check test preconditions.
+ asserts.abort_class_if(
+ not self.client.isAtLeastV(),
+ "Do not enforce the test until V+ since chipset potential bugs are"
+ " expected to be fixed on V+ releases.",
+ )
+ tether_utils.assume_hotspot_test_preconditions(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ asserts.abort_class_if(
+ not apf_utils.is_send_raw_packet_downstream_supported(
+ self.serverDevice
+ ),
+ "NetworkStack is too old to support send raw packet, skip test.",
+ )
+
+ # Fetch device properties and storing them locally for later use.
+ self.server_iface_name, client_network = (
+ tether_utils.setup_hotspot_and_client_for_upstream_type(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ )
+ self.client_iface_name = self.client.getInterfaceNameFromNetworkHandle(
+ client_network
+ )
+ self.server_mac_address = apf_utils.get_hardware_address(
+ self.serverDevice, self.server_iface_name
+ )
+
+ # Enable doze mode to activate APF.
+ adb_utils.set_doze_mode(self.clientDevice, True)
+
+ def teardown_class(self):
+ adb_utils.set_doze_mode(self.clientDevice, False)
+ tether_utils.cleanup_tethering_for_upstream_type(
+ self.serverDevice, UpstreamType.NONE
+ )
+
+ def send_packet_and_expect_counter_increased(
+ self, packet: str, counter_name: str
+ ) -> None:
+ count_before_test = apf_utils.get_apf_counter(
+ self.clientDevice,
+ self.client_iface_name,
+ counter_name,
+ )
+ apf_utils.send_raw_packet_downstream(
+ self.serverDevice, self.server_iface_name, packet
+ )
+
+ assert_utils.expect_with_retry(
+ lambda: apf_utils.get_apf_counter(
+ self.clientDevice,
+ self.client_iface_name,
+ counter_name,
+ )
+ > count_before_test
+ )
+
+ # TODO: Verify the packet is not actually received.
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
new file mode 100644
index 0000000..e84ba3e
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -0,0 +1,306 @@
+# 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.
+
+from dataclasses import dataclass
+import re
+from mobly import asserts
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib.adb import AdbError
+from net_tests_utils.host.python import adb_utils, assert_utils
+
+
+class PatternNotFoundException(Exception):
+ """Raised when the given pattern cannot be found."""
+
+
+class UnsupportedOperationException(Exception):
+ pass
+
+
+def get_apf_counter(
+ ad: android_device.AndroidDevice, iface: str, counter_name: str
+) -> int:
+ counters = get_apf_counters_from_dumpsys(ad, iface)
+ return counters.get(counter_name, 0)
+
+
+def get_apf_counters_from_dumpsys(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> dict:
+ dumpsys = adb_utils.get_dumpsys_for_service(ad, "network_stack")
+
+ # Extract IpClient section of the specified interface.
+ # This takes inputs like:
+ # IpClient.wlan0
+ # ...
+ # IpClient.wlan1
+ # ...
+ iface_pattern = re.compile(
+ r"^IpClient\." + iface_name + r"\n" + r"((^\s.*\n)+)", re.MULTILINE
+ )
+ iface_result = iface_pattern.search(dumpsys)
+ if iface_result is None:
+ raise PatternNotFoundException("Cannot find IpClient for " + iface_name)
+
+ # Extract APF counters section from IpClient section, which looks like:
+ # APF packet counters:
+ # COUNTER_NAME: VALUE
+ # ....
+ apf_pattern = re.compile(
+ r"APF packet counters:.*\n.(\s+[A-Z_0-9]+: \d+\n)+", re.MULTILINE
+ )
+ apf_result = apf_pattern.search(iface_result.group(0))
+ if apf_result is None:
+ raise PatternNotFoundException(
+ "Cannot find APF counters in text: " + iface_result.group(0)
+ )
+
+ # Extract key-value pairs from APF counters section into a list of tuples,
+ # e.g. [('COUNTER1', '1'), ('COUNTER2', '2')].
+ counter_pattern = re.compile(r"(?P<name>[A-Z_0-9]+): (?P<value>\d+)")
+ counter_result = counter_pattern.findall(apf_result.group(0))
+ if counter_result is None:
+ raise PatternNotFoundException(
+ "Cannot extract APF counters in text: " + apf_result.group(0)
+ )
+
+ # Convert into a dict.
+ result = {}
+ for key, value_str in counter_result:
+ result[key] = int(value_str)
+
+ ad.log.debug("Getting apf counters: " + str(result))
+ return result
+
+def get_ipv4_addresses(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> list[str]:
+ """Retrieves the IPv4 addresses of a given interface on an Android device.
+
+ This function executes an ADB shell command (`ip -4 address show`) to get the
+ network interface information and extracts the IPv4 address from the output.
+ If devices have no IPv4 address, raise PatternNotFoundException.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+
+ Returns:
+ The IPv4 addresses of the interface as a list of string.
+ Return empty list if no IPv4 address.
+ """
+ # output format:
+ # 54: wlan2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
+ # inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan2
+ # valid_lft forever preferred_lft forever
+ # inet 192.168.1.1/24 brd 192.168.1.255 scope global wlan2
+ # valid_lft forever preferred_lft forever
+ output = adb_utils.adb_shell(ad, f"ip -4 address show {iface_name}")
+ pattern = r"inet\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/\d+"
+ matches = re.findall(pattern, output)
+
+ if matches:
+ return matches
+ else:
+ return []
+
+def get_ipv6_addresses(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> list[str]:
+ """Retrieves the IPv6 addresses of a given interface on an Android device.
+
+ This function executes an ADB shell command (`ip -6 address show`) to get the
+ network interface information and extracts the IPv6 address from the output.
+ If devices have no IPv6 address, raise PatternNotFoundException.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+
+ Returns:
+ The IPv6 addresses of the interface as a list of string.
+ Return empty list if no IPv6 address.
+ """
+ # output format
+ # 54: wlan2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
+ # inet6 fe80::10a3:5dff:fe52:de32/64 scope link
+ # valid_lft forever preferred_lft forever
+ # inet6 fe80::1233:aadb:3d32:1234/64 scope link
+ # valid_lft forever preferred_lft forever
+ output = adb_utils.adb_shell(ad, f"ip -6 address show {iface_name}")
+ pattern = r"inet6\s+([0-9a-fA-F:]+)\/\d+"
+ matches = re.findall(pattern, output)
+
+ if matches:
+ return matches
+ else:
+ return []
+
+def get_hardware_address(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> str:
+ """Retrieves the hardware (MAC) address for a given network interface.
+
+ Returns:
+ The hex representative of the MAC address in uppercase.
+ E.g. 12:34:56:78:90:AB
+
+ Raises:
+ PatternNotFoundException: If the MAC address is not found in the command
+ output.
+ """
+
+ # Run the "ip link" command and get its output.
+ ip_link_output = adb_utils.adb_shell(ad, f"ip link show {iface_name}")
+
+ # Regular expression to extract the MAC address.
+ # Parse hardware address from ip link output like below:
+ # 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
+ # link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+ pattern = r"link/ether (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
+ match = re.search(pattern, ip_link_output)
+
+ if match:
+ return match.group(1).upper() # Extract the MAC address string.
+ else:
+ raise PatternNotFoundException(
+ "Cannot get hardware address for " + iface_name
+ )
+
+
+def is_send_raw_packet_downstream_supported(
+ ad: android_device.AndroidDevice,
+) -> bool:
+ try:
+ # Invoke the shell command with empty argument and see how NetworkStack respond.
+ # If supported, an IllegalArgumentException with help page will be printed.
+ send_raw_packet_downstream(ad, "", "")
+ except assert_utils.UnexpectedBehaviorError:
+ return True
+ except UnsupportedOperationException:
+ return False
+
+
+def send_raw_packet_downstream(
+ ad: android_device.AndroidDevice,
+ iface_name: str,
+ packet_in_hex: str,
+) -> None:
+ """Sends a raw packet over the specified downstream interface.
+
+ This function constructs and sends a raw packet using the
+ `send-raw-packet-downstream`
+ command provided by NetworkStack process. It's primarily intended for testing
+ purposes.
+
+ Args:
+ ad: The AndroidDevice object representing the connected device.
+ iface_name: The name of the network interface to use (e.g., "wlan0",
+ "eth0").
+ packet_in_hex: The raw packet data starting from L2 header encoded in
+ hexadecimal string format.
+
+ Raises:
+ UnsupportedOperationException: If the NetworkStack doesn't support
+ the `send-raw-packet` command.
+ UnexpectedBehaviorException: If the command execution produces unexpected
+ output other than an empty response or "Unknown command".
+
+ Important Considerations:
+ Security: This method only works on tethering downstream interfaces due
+ to security restrictions.
+ Packet Format: The `packet_in_hex` must be a valid hexadecimal
+ representation of a packet starting from L2 header.
+ """
+
+ cmd = (
+ "cmd network_stack send-raw-packet-downstream"
+ f" {iface_name} {packet_in_hex}"
+ )
+
+ # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+ try:
+ output = adb_utils.adb_shell(ad, cmd)
+ except AdbError as e:
+ output = str(e.stdout)
+ if output:
+ if "Unknown command" in output:
+ raise UnsupportedOperationException(
+ "send-raw-packet-downstream command is not supported."
+ )
+ raise assert_utils.UnexpectedBehaviorError(
+ f"Got unexpected output: {output} for command: {cmd}."
+ )
+
+
+@dataclass
+class ApfCapabilities:
+ """APF program support capabilities.
+
+ See android.net.apf.ApfCapabilities.
+
+ Attributes:
+ apf_version_supported (int): Version of APF instruction set supported for
+ packet filtering. 0 indicates no support for packet filtering using APF
+ programs.
+ apf_ram_size (int): Size of APF ram.
+ apf_packet_format (int): Format of packets passed to APF filter. Should be
+ one of ARPHRD_*
+ """
+
+ apf_version_supported: int
+ apf_ram_size: int
+ apf_packet_format: int
+
+ def __init__(
+ self,
+ apf_version_supported: int,
+ apf_ram_size: int,
+ apf_packet_format: int,
+ ):
+ self.apf_version_supported = apf_version_supported
+ self.apf_ram_size = apf_ram_size
+ self.apf_packet_format = apf_packet_format
+
+ def __str__(self):
+ """Returns a user-friendly string representation of the APF capabilities."""
+ return (
+ f"APF Version: {self.apf_version_supported}\n"
+ f"Ram Size: {self.apf_ram_size} bytes\n"
+ f"Packet Format: {self.apf_packet_format}"
+ )
+
+
+def get_apf_capabilities(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> ApfCapabilities:
+ output = adb_utils.adb_shell(
+ ad, f"cmd network_stack apf {iface_name} capabilities"
+ )
+ try:
+ values = [int(value_str) for value_str in output.split(",")]
+ except ValueError:
+ return ApfCapabilities(0, 0, 0) # Conversion to integer failed
+ return ApfCapabilities(values[0], values[1], values[2])
+
+
+def assume_apf_version_support_at_least(
+ ad: android_device.AndroidDevice, iface_name: str, expected_version: int
+) -> None:
+ caps = get_apf_capabilities(ad, iface_name)
+ asserts.abort_class_if(
+ caps.apf_version_supported < expected_version,
+ f"Supported apf version {caps.apf_version_supported} < expected version"
+ f" {expected_version}",
+ )
diff --git a/staticlibs/testutils/host/python/assert_utils.py b/staticlibs/testutils/host/python/assert_utils.py
new file mode 100644
index 0000000..da1bb9e
--- /dev/null
+++ b/staticlibs/testutils/host/python/assert_utils.py
@@ -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.
+
+import time
+from typing import Callable
+
+
+class UnexpectedBehaviorError(Exception):
+ """Raised when there is an unexpected behavior during applying a procedure."""
+
+
+def expect_with_retry(
+ predicate: Callable[[], bool],
+ retry_action: Callable[[], None] = None,
+ max_retries: int = 10,
+ retry_interval_sec: int = 1,
+) -> None:
+ """Executes a predicate and retries if it doesn't return True."""
+
+ for retry in range(max_retries):
+ if predicate():
+ return None
+ else:
+ if retry == max_retries - 1:
+ break
+ if retry_action:
+ retry_action()
+ time.sleep(retry_interval_sec)
+
+ raise UnexpectedBehaviorError(
+ "Predicate didn't become true after " + str(max_retries) + " retries."
+ )
diff --git a/staticlibs/testutils/host/python/mdns_utils.py b/staticlibs/testutils/host/python/mdns_utils.py
new file mode 100644
index 0000000..1234e54
--- /dev/null
+++ b/staticlibs/testutils/host/python/mdns_utils.py
@@ -0,0 +1,57 @@
+# 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.
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+
+def assume_mdns_test_preconditions(
+ advertising_device: android_device, discovery_device: android_device
+) -> None:
+ advertising = advertising_device.connectivity_multi_devices_snippet
+ discovery = discovery_device.connectivity_multi_devices_snippet
+
+ asserts.skip_if(
+ not advertising.isAtLeastT(), "Advertising device SDK is lower than T."
+ )
+ asserts.skip_if(
+ not discovery.isAtLeastT(), "Discovery device SDK is lower than T."
+ )
+
+
+def register_mdns_service_and_discover_resolve(
+ advertising_device: android_device, discovery_device: android_device
+) -> None:
+ """Test mdns advertising, discovery and resolution
+
+ One device registers an mDNS service, and another device discovers and
+ resolves that service.
+ """
+ advertising = advertising_device.connectivity_multi_devices_snippet
+ discovery = discovery_device.connectivity_multi_devices_snippet
+
+ # Register a mDns service
+ advertising.registerMDnsService()
+
+ # Ensure the discovery and resolution of the mDNS service
+ discovery.ensureMDnsServiceDiscoveryAndResolution()
+
+
+def cleanup_mdns_service(
+ advertising_device: android_device, discovery_device: android_device
+) -> None:
+ # Unregister the mDns service
+ advertising_device.connectivity_multi_devices_snippet.unregisterMDnsService()
+ # Stop discovery
+ discovery_device.connectivity_multi_devices_snippet.stopMDnsServiceDiscovery()
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
new file mode 100644
index 0000000..677329a
--- /dev/null
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -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.
+#
+# 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.
+
+from mobly import base_test
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class MultiDevicesTestBase(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,
+ )
+ self.client = self.clientDevice.connectivity_multi_devices_snippet
diff --git a/staticlibs/testutils/host/python/packet_utils.py b/staticlibs/testutils/host/python/packet_utils.py
new file mode 100644
index 0000000..b613f03
--- /dev/null
+++ b/staticlibs/testutils/host/python/packet_utils.py
@@ -0,0 +1,70 @@
+# 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.
+from ipaddress import IPv4Address
+from socket import inet_aton
+
+ETHER_BROADCAST_MAC_ADDRESS = "FF:FF:FF:FF:FF:FF"
+ARP_REQUEST_OP = 1
+ARP_REPLY_OP = 2
+
+"""
+This variable defines a template for constructing ARP packets in hexadecimal format.
+It's used to provide the common fields for ARP packet, and replaced needed fields when constructing
+"""
+ARP_TEMPLATE = (
+ # Ether Header (14 bytes)
+ "{dst_mac}" + # DA
+ "{src_mac}" + # SA
+ "0806" + # ARP
+ # ARP Header (28 bytes)
+ "0001" + # Hardware type (Ethernet)
+ "0800" + # Protocol type (IPv4)
+ "06" + # hardware address length
+ "04" + # protocol address length
+ "{opcode}" + # opcode
+ "{sender_mac}" + # sender MAC
+ "{sender_ip}" + # sender IP
+ "{target_mac}" + # target MAC
+ "{target_ip}" # target IP
+)
+
+def construct_arp_packet(src_mac, dst_mac, src_ip, dst_ip, op) -> str:
+ """Constructs an ARP packet as a hexadecimal string.
+
+ This function creates an ARP packet by filling in the required fields
+ in a predefined ARP packet template.
+
+ Args:
+ src_mac: The MAC address of the sender. (e.g. "11:22:33:44:55:66")
+ dst_mac: The MAC address of the recipient. (e.g. "aa:bb:cc:dd:ee:ff")
+ src_ip: The IP address of the sender. (e.g. "1.1.1.1")
+ dst_ip: The IP address of the target machine. (e.g. "2.2.2.2")
+ op: The op code of the ARP packet, refer to ARP_*_OP
+
+ Returns:
+ A string representing the ARP packet in hexadecimal format.
+ """
+ # Replace the needed fields from packet template
+ arp_pkt = ARP_TEMPLATE.format(
+ dst_mac=dst_mac.replace(":",""),
+ src_mac=src_mac.replace(":",""),
+ opcode=str(op).rjust(4, "0"),
+ sender_mac=src_mac.replace(":",""),
+ sender_ip=inet_aton(src_ip).hex(),
+ target_mac=("000000000000" if op == ARP_REQUEST_OP else dst_mac.replace(":", "")),
+ target_ip=inet_aton(dst_ip).hex()
+ )
+
+ # always convert to upper case hex string
+ return arp_pkt.upper()
\ No newline at end of file
diff --git a/tests/cts/multidevices/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
similarity index 86%
rename from tests/cts/multidevices/tether_utils.py
rename to staticlibs/testutils/host/python/tether_utils.py
index 61f5c43..702b596 100644
--- a/tests/cts/multidevices/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -11,18 +11,6 @@
# 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.
-#
-# 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.
import base64
import uuid
@@ -32,6 +20,7 @@
class UpstreamType:
+ NONE = 0
CELLULAR = 1
WIFI = 2
@@ -68,6 +57,8 @@
not server.isStaApConcurrencySupported(),
"Server requires Wifi AP + STA concurrency",
)
+ elif upstream_type == UpstreamType.NONE:
+ pass
else:
raise ValueError(f"Invalid upstream type: {upstream_type}")
@@ -90,6 +81,8 @@
server.requestCellularAndEnsureDefault()
elif upstream_type == UpstreamType.WIFI:
server.ensureWifiIsDefault()
+ elif upstream_type == UpstreamType.NONE:
+ pass
else:
raise ValueError(f"Invalid upstream type: {upstream_type}")
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
new file mode 100644
index 0000000..ef6af75
--- /dev/null
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -0,0 +1,93 @@
+# 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.
+
+from mobly import asserts
+from mobly.controllers import android_device
+from net_tests_utils.host.python import tether_utils
+
+
+def assume_wifi_p2p_test_preconditions(
+ server_device: android_device, client_device: android_device
+) -> None:
+ """Preconditions check for running Wi-Fi P2P test."""
+ server = server_device.connectivity_multi_devices_snippet
+ client = client_device.connectivity_multi_devices_snippet
+
+ # Assert pre-conditions
+ asserts.skip_if(not server.hasWifiFeature(), "Server requires Wifi feature")
+ asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+ asserts.skip_if(
+ not server.isP2pSupported(), "Server requires Wi-fi P2P feature"
+ )
+ asserts.skip_if(
+ not client.isP2pSupported(), "Client requires Wi-fi P2P feature"
+ )
+
+
+def setup_wifi_p2p_server_and_client(
+ server_device: android_device, client_device: android_device
+) -> None:
+ """Set up the Wi-Fi P2P server and client, then connect them to establish a Wi-Fi P2P connection."""
+ server = server_device.connectivity_multi_devices_snippet
+ client = client_device.connectivity_multi_devices_snippet
+
+ # Start Wi-Fi P2P on both server and client.
+ server.startWifiP2p()
+ client.startWifiP2p()
+
+ # Get the current device name
+ server_name = server.getDeviceName()
+ client_name = client.getDeviceName()
+
+ # Generate Wi-Fi P2P group passphrase with random characters.
+ group_name = "DIRECT-" + tether_utils.generate_uuid32_base64()
+ group_passphrase = tether_utils.generate_uuid32_base64()
+
+ # Server creates a Wi-Fi P2P group
+ server.createGroup(group_name, group_passphrase)
+
+ # Start Wi-Fi P2p peers discovery on both devices
+ server.startPeersDiscovery()
+ client.startPeersDiscovery()
+
+ # Ensure the target device has been discovered
+ server_address = client.ensureDeviceDiscovered(server_name)
+ client_address = server.ensureDeviceDiscovered(client_name)
+
+ # Server invites the device to the group
+ server.inviteDeviceToGroup(group_name, group_passphrase, client_address)
+
+ # Wait for a p2p connection changed intent to ensure the invitation has been
+ # received.
+ client.waitForP2pConnectionChanged(True, group_name)
+ # Accept the group invitation
+ client.acceptGroupInvitation(server_address)
+
+ # Server waits for connection request from client and accept joining
+ server.waitForPeerConnectionRequestAndAcceptJoining(client_address)
+
+ # Wait for a p2p connection changed intent to ensure joining the group
+ client.waitForP2pConnectionChanged(False, group_name)
+
+ # Ensure Wi-Fi P2P connected on both devices
+ client.ensureDeviceConnected(server_name)
+ server.ensureDeviceConnected(client_name)
+
+
+def cleanup_wifi_p2p(
+ server_device: android_device, client_device: android_device
+) -> None:
+ # Stop Wi-Fi P2P
+ server_device.connectivity_multi_devices_snippet.stopWifiP2p()
+ client_device.connectivity_multi_devices_snippet.stopWifiP2p()
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index 8cef6aa..17f5e96 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,18 +28,26 @@
import android.net.NetworkStats.SET_DEFAULT
import android.net.NetworkStats.SET_FOREGROUND
import android.net.NetworkStats.TAG_NONE
+import android.os.Build
import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.assertNetworkStatsEquals
import com.android.testutils.assertParcelingIsLossless
import kotlin.test.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-@RunWith(JUnit4::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
class NetworkStatsApiTest {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
private val testStatsEmpty = NetworkStats(0L, 0)
// Note that these variables need to be initialized outside of constructor, initialize
@@ -49,6 +57,7 @@
// be merged if performing add on these 2 stats.
private lateinit var testStats1: NetworkStats
private lateinit var testStats2: NetworkStats
+ private lateinit var expectedEntriesInStats2: List<Entry>
// This is a result of adding stats1 and stats2, while the merging of common key items is
// subject to test later, this should not be initialized with for a loop to add stats1
@@ -84,19 +93,23 @@
METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0))
assertEquals(8, testStats1.size())
- testStats2 = NetworkStats(0L, 0)
- // Entries which are common for set1 and set2.
- .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1))
- .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45))
- .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7))
- .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0))
- // Entry which only appears in set2.
- .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+ expectedEntriesInStats2 = listOf(
+ // Entries which are common for set1 and set2.
+ Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
+ Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
+ Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
+ Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
+ // Entry which only appears in set2.
+ Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+ testStats2 = NetworkStats(0L, 5)
+ for (entry in expectedEntriesInStats2) {
+ testStats2 = testStats2.addEntry(entry)
+ }
assertEquals(5, testStats2.size())
testStats3 = NetworkStats(0L, 9)
@@ -125,18 +138,6 @@
@Test
fun testAddEntry() {
- val expectedEntriesInStats2 = arrayOf(
- Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
- Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
- Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
- Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
- Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
-
// While testStats* are already initialized with addEntry, verify content added
// matches expectation.
for (i in expectedEntriesInStats2.indices) {
@@ -150,6 +151,27 @@
assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9),
stats.getValues(3, null))
+
+ // Verify the original ststs object is not altered.
+ for (i in expectedEntriesInStats2.indices) {
+ val entry = testStats2.getValues(i, null)
+ assertEquals(expectedEntriesInStats2[i], entry)
+ }
+ }
+
+ @ConnectivityModuleTest
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2) // Mainlined NetworkStats only runs on T+
+ @Test
+ fun testAddEntries() {
+ val baseStats = NetworkStats(0L, 1)
+ .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+ val statsUnderTest = baseStats.addEntries(expectedEntriesInStats2)
+ // Assume the correctness of addEntry is verified in other tests.
+ val expectedStats = testStats2
+ .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+ assertNetworkStatsEquals(expectedStats, statsUnderTest)
}
@Test
diff --git a/tests/cts/hostside-network-policy/Android.bp b/tests/cts/hostside-network-policy/Android.bp
deleted file mode 100644
index c3ce0b9..0000000
--- a/tests/cts/hostside-network-policy/Android.bp
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
- default_team: "trendy_team_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_test_host {
- name: "CtsHostsideNetworkPolicyTests",
- defaults: ["cts_defaults"],
- // Only compile source java files in this apk.
- srcs: [
- "src/**/*.java",
- ":ArgumentConstants",
- ],
- libs: [
- "cts-tradefed",
- "tradefed",
- ],
- static_libs: [
- "modules-utils-build-testing",
- ],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
- data: [
- ":CtsHostsideNetworkPolicyTestsApp",
- ":CtsHostsideNetworkPolicyTestsApp2",
- ],
- per_testcase_directory: true,
-}
diff --git a/tests/cts/hostside-network-policy/AndroidTest.xml b/tests/cts/hostside-network-policy/AndroidTest.xml
deleted file mode 100644
index 44f77f8..0000000
--- a/tests/cts/hostside-network-policy/AndroidTest.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?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 network policy host 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="instant_app" />
- <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
- <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
-
- <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
- <target_preparer class="com.android.cts.netpolicy.NetworkPolicyTestsPreparer" />
-
- <!-- Enabling change id ALLOW_TEST_API_ACCESS allows that package to access @TestApi methods -->
- <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
- <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
- <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
- <option name="teardown-command" value="cmd power set-mode 0" />
- <option name="teardown-command" value="cmd battery reset" />
- <option name="teardown-command" value="cmd netpolicy stop-watching" />
- </target_preparer>
-
- <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
- <option name="force-skip-system-props" value="true" />
- <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
- <option name="set-global-setting" key="low_power_standby_enabled" value="0" />
- </target_preparer>
-
- <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
- <option name="jar" value="CtsHostsideNetworkPolicyTests.jar" />
- <option name="runtime-hint" value="3m56s" />
- </test>
-
- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
- <option name="directory-keys" value="/sdcard/CtsHostsideNetworkPolicyTests" />
- <option name="collect-on-run-ended-only" value="true" />
- </metrics_collector>
-</configuration>
diff --git a/tests/cts/hostside-network-policy/OWNERS b/tests/cts/hostside-network-policy/OWNERS
deleted file mode 100644
index ea83e61..0000000
--- a/tests/cts/hostside-network-policy/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 61373
-# Inherits parent owners
-include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
diff --git a/tests/cts/hostside-network-policy/TEST_MAPPING b/tests/cts/hostside-network-policy/TEST_MAPPING
deleted file mode 100644
index 57ac4f7..0000000
--- a/tests/cts/hostside-network-policy/TEST_MAPPING
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "presubmit-large": [
- {
- "name": "CtsHostsideNetworkPolicyTests",
- "options": [
- {
- "exclude-annotation": "android.platform.test.annotations.FlakyTest"
- },
- {
- "exclude-annotation": "android.platform.test.annotations.RequiresDevice"
- }
- ]
- }
- ],
- "postsubmit": [
- {
- // Postsubmit on virtual devices to monitor flakiness of all tests that don't require a
- // physical device
- "name": "CtsHostsideNetworkPolicyTests",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.RequiresDevice"
- }
- ]
- }
- ]
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
deleted file mode 100644
index 068d9d8..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
+++ /dev/null
@@ -1,33 +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.cts.netpolicy.hostside;
-
-import android.app.job.JobInfo;
-
-import com.android.cts.netpolicy.hostside.INetworkCallback;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-interface IMyService {
- void registerBroadcastReceiver();
- int getCounters(String receiverName, String action);
- NetworkCheckResult checkNetworkStatus(String customUrl);
- String getRestrictBackgroundStatus();
- void sendNotification(int notificationId, String notificationType);
- void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
- void unregisterNetworkCallback();
- int scheduleJob(in JobInfo jobInfo);
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
deleted file mode 100644
index 38efc7b..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-
-interface INetworkCallback {
- void onBlockedStatusChanged(in Network network, boolean blocked);
- void onAvailable(in Network network);
- void onLost(in Network network);
- void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap);
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
deleted file mode 100644
index c6b7a1c..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
+++ /dev/null
@@ -1,30 +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.cts.netpolicy.hostside;
-
-import android.net.NetworkInfo;
-
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-interface INetworkStateObserver {
- void onNetworkStateChecked(int resultCode, in NetworkCheckResult networkCheckResult);
-
- const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0;
- const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1;
- const int RESULT_ERROR_UNEXPECTED_CAPABILITIES = 2;
- const int RESULT_ERROR_OTHER = 3;
-}
\ No newline at end of file
diff --git a/tests/cts/hostside-network-policy/app/Android.bp b/tests/cts/hostside-network-policy/app/Android.bp
deleted file mode 100644
index a31c843..0000000
--- a/tests/cts/hostside-network-policy/app/Android.bp
+++ /dev/null
@@ -1,57 +0,0 @@
-//
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
- default_team: "trendy_team_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_defaults {
- name: "CtsHostsideNetworkPolicyTestsAppDefaults",
- platform_apis: true,
- static_libs: [
- "CtsHostsideNetworkPolicyTestsAidl",
- "androidx.test.ext.junit",
- "androidx.test.rules",
- "androidx.test.uiautomator_uiautomator",
- "compatibility-device-util-axt",
- "cts-net-utils",
- "ctstestrunner-axt",
- "modules-utils-build",
- ],
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
- srcs: [
- "src/**/*.java",
- ":ArgumentConstants",
- ],
- // Tag this module as a cts test artifact
- test_suites: [
- "general-tests",
- "sts",
- ],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkPolicyTestsApp",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- "CtsHostsideNetworkPolicyTestsAppDefaults",
- ],
-}
diff --git a/tests/cts/hostside-network-policy/app/AndroidManifest.xml b/tests/cts/hostside-network-policy/app/AndroidManifest.xml
deleted file mode 100644
index f19e35f..0000000
--- a/tests/cts/hostside-network-policy/app/AndroidManifest.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?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.android.cts.netpolicy.hostside">
-
- <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_WIFI_STATE"/>
- <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.WAKE_LOCK" />
-
- <application android:requestLegacyExternalStorage="true">
- <uses-library android:name="android.test.runner"/>
- <service android:name=".MyNotificationListenerService"
- android:label="MyNotificationListenerService"
- android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
- android:exported="true">
- <intent-filter>
- <action android:name="android.service.notification.NotificationListenerService"/>
- </intent-filter>
- </service>
- </application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.cts.netpolicy.hostside"/>
-
-</manifest>
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
deleted file mode 100644
index 19e4364..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
+++ /dev/null
@@ -1,201 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered tests on idle apps.
- */
-@RequiredProperties({APP_STANDBY_MODE})
-abstract class AbstractAppIdleTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- setAppIdle(false);
- turnBatteryOn();
-
- registerBroadcastReceiver();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- resetBatteryState();
- setAppIdle(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_enabled() throws Exception {
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground app doesn't lose access upon enabling it.
- setAppIdle(true);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- finishActivity();
- assertAppIdle(false); // verify - not idle anymore, since activity was launched...
- assertBackgroundNetworkAccess(true);
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- // Same for foreground service.
- setAppIdle(true);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- stopForegroundService();
- assertAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- // Set Idle after foreground service start.
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setAppIdle(true);
- addPowerSaveModeWhitelist(TEST_PKG);
- removePowerSaveModeWhitelist(TEST_PKG);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- }
-
- @Test
- public void testBackgroundNetworkAccess_whitelisted() throws Exception {
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertAppIdle(false); // verify - not idle anymore, since whitelisted
- assertBackgroundNetworkAccess(true);
-
- setAppIdleNoAssert(true);
- assertAppIdle(false); // app is still whitelisted
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertAppIdle(true); // verify - idle again, once whitelisted was removed
- assertBackgroundNetworkAccess(false);
-
- setAppIdle(true);
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertAppIdle(false); // verify - not idle anymore, since whitelisted
- assertBackgroundNetworkAccess(true);
-
- setAppIdleNoAssert(true);
- assertAppIdle(false); // app is still whitelisted
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertAppIdle(true); // verify - idle again, once whitelisted was removed
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
-
- // verify - no whitelist, no access!
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception {
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_disabled() throws Exception {
- assertBackgroundNetworkAccess(true);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- }
-
- @RequiredProperties({BATTERY_SAVER_MODE})
- @Test
- public void testAppIdleNetworkAccess_whenCharging() throws Exception {
- // Check that app is paroled when charging
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
- turnBatteryOff();
- assertBackgroundNetworkAccess(true);
- turnBatteryOn();
- assertBackgroundNetworkAccess(false);
-
- // Check that app is restricted when not idle but power-save is on
- setAppIdle(false);
- assertBackgroundNetworkAccess(true);
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
- // Use setBatterySaverMode API to leave power-save mode instead of plugging in charger
- setBatterySaverMode(false);
- turnBatteryOff();
- assertBackgroundNetworkAccess(true);
-
- // And when no longer charging, it still has network access, since it's not idle
- turnBatteryOn();
- assertBackgroundNetworkAccess(true);
- }
-
- @Test
- public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception {
- setAppIdle(true);
- assertAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(true);
-
- removeAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- // Make sure whitelisting a random app doesn't affect the tested app.
- addAppIdleWhitelist(mUid + 1);
- assertBackgroundNetworkAccess(false);
- removeAppIdleWhitelist(mUid + 1);
- }
-
- @Test
- public void testAppIdle_toast() throws Exception {
- setAppIdle(true);
- assertAppIdle(true);
- assertEquals("Shown", showToast());
- assertAppIdle(true);
- // Wait for a couple of seconds for the toast to actually be shown
- SystemClock.sleep(2000);
- assertAppIdle(true);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
deleted file mode 100644
index ae226e2..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
+++ /dev/null
@@ -1,111 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Battery Saver Mode tests.
- */
-@RequiredProperties({BATTERY_SAVER_MODE})
-abstract class AbstractBatterySaverModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- setBatterySaverMode(false);
-
- registerBroadcastReceiver();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- setBatterySaverMode(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_enabled() throws Exception {
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground app doesn't lose access upon Battery Saver.
- setBatterySaverMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- setBatterySaverMode(true);
- assertTopNetworkAccess(true);
-
- // Although it should not have access while the screen is off.
- turnScreenOff();
- assertBackgroundNetworkAccess(false);
- turnScreenOn();
- assertTopNetworkAccess(true);
-
- // Goes back to background state.
- finishActivity();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground service doesn't lose access upon enabling Battery Saver.
- setBatterySaverMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setBatterySaverMode(true);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_whitelisted() throws Exception {
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_disabled() throws Exception {
- assertBackgroundNetworkAccess(true);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
deleted file mode 100644
index 00f67f4..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
-
-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();
-
- stopApp();
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- }
-
- @Test
- public void testFgsNetworkAccess() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- }
-
- @Test
- public void testActivityNetworkAccess() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- }
-
- @Test
- public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- assertNetworkAccess(true, null);
- }
-
- @Test
- public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- assertNetworkAccess(true, null);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
deleted file mode 100644
index 0c8cb70..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
+++ /dev/null
@@ -1,143 +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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.NOT_LOW_RAM_DEVICE;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Doze Mode tests.
- */
-@RequiredProperties({DOZE_MODE})
-abstract class AbstractDozeModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- setDozeMode(false);
-
- registerBroadcastReceiver();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- setDozeMode(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_enabled() throws Exception {
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground service doesn't lose network access upon enabling doze.
- setDozeMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setDozeMode(true);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_whitelisted() throws Exception {
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_disabled() throws Exception {
- assertBackgroundNetworkAccess(true);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- }
-
- @RequiredProperties({NOT_LOW_RAM_DEVICE})
- @Test
- public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
- throws Exception {
- setPendingIntentAllowlistDuration(NETWORK_TIMEOUT_MS);
- try {
- registerNotificationListenerService();
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
-
- testNotification(4, NOTIFICATION_TYPE_CONTENT);
- testNotification(8, NOTIFICATION_TYPE_DELETE);
- testNotification(15, NOTIFICATION_TYPE_FULL_SCREEN);
- testNotification(16, NOTIFICATION_TYPE_BUNDLE);
- testNotification(23, NOTIFICATION_TYPE_ACTION);
- testNotification(42, NOTIFICATION_TYPE_ACTION_BUNDLE);
- testNotification(108, NOTIFICATION_TYPE_ACTION_REMOTE_INPUT);
- } finally {
- resetDeviceIdleSettings();
- }
- }
-
- private void testNotification(int id, String type) throws Exception {
- sendNotification(id, type);
- assertBackgroundNetworkAccess(true);
- if (type.equals(NOTIFICATION_TYPE_ACTION)) {
- // Make sure access is disabled after it expires. Since this check considerably slows
- // downs the CTS tests, do it just once.
- SystemClock.sleep(NETWORK_TIMEOUT_MS);
- assertBackgroundNetworkAccess(false);
- }
- }
-
- // Must override so it only tests foreground service - once an app goes to foreground, device
- // leaves Doze Mode.
- @Override
- protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- stopForegroundService();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
deleted file mode 100644
index 5435920..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class AbstractExpeditedJobTest extends AbstractRestrictBackgroundNetworkTestCase {
- @Before
- public final void setUp() throws Exception {
- super.setUp();
- resetDeviceState();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
- resetDeviceState();
- }
-
- private void resetDeviceState() throws Exception {
- resetBatteryState();
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setAppIdle(false);
- setDozeMode(false);
- }
-
- @Test
- @RequiredProperties({BATTERY_SAVER_MODE})
- public void testNetworkAccess_batterySaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
- public void testNetworkAccess_dataSaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-
- @Test
- @RequiredProperties({APP_STANDBY_MODE})
- public void testNetworkAccess_appIdleState() throws Exception {
- turnBatteryOn();
- setAppIdle(false);
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DOZE_MODE})
- public void testNetworkAccess_dozeMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
- public void testNetworkAccess_dataAndBatterySaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DOZE_MODE, DATA_SAVER_MODE, METERED_NETWORK})
- public void testNetworkAccess_dozeAndDataSaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK, DOZE_MODE,
- APP_STANDBY_MODE})
- public void testNetworkAccess_allRestrictionsEnabled() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- setBatterySaverMode(true);
- setAppIdle(true);
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
deleted file mode 100644
index 0f5f58c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ /dev/null
@@ -1,1165 +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.cts.netpolicy.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.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.executeShellCommand;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.forceRunJob;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getConnectivityManager;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getContext;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getInstrumentation;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackgroundInternal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.annotation.NonNull;
-import android.app.Instrumentation;
-import android.app.NotificationManager;
-import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkInfo.State;
-import android.net.NetworkRequest;
-import android.os.BatteryManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.PowerManager;
-import android.os.RemoteCallback;
-import android.os.SystemClock;
-import android.provider.DeviceConfig;
-import android.service.notification.NotificationListenerService;
-import android.util.Log;
-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;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-
-/**
- * Superclass for tests related to background network restrictions.
- */
-@RunWith(NetworkPolicyTestRunner.class)
-public abstract class AbstractRestrictBackgroundNetworkTestCase {
- public static final String TAG = "RestrictBackgroundNetworkTests";
-
- protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
- protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
- // TODO(b/321797685): Configure it via device-config once it is available.
- protected final long mProcessStateTransitionLongDelayMs =
- useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(20)
- : TimeUnit.SECONDS.toMillis(5);
- protected final long mProcessStateTransitionShortDelayMs =
- useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(2)
- : 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";
- private static final String TEST_APP2_JOB_SERVICE_CLASS = TEST_APP2_PKG + ".MyJobService";
-
- 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;
-
- // Constants below must match values defined on app2's Common.java
- private static final String MANIFEST_RECEIVER = "ManifestReceiver";
- private static final String DYNAMIC_RECEIVER = "DynamicReceiver";
- private static final String ACTION_FINISH_ACTIVITY =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
- private static final String ACTION_FINISH_JOB =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
- // Copied from com.android.server.net.NetworkPolicyManagerService class
- private static final String ACTION_SNOOZE_WARNING =
- "com.android.server.net.action.SNOOZE_WARNING";
-
- private static final String ACTION_RECEIVER_READY =
- "com.android.cts.netpolicy.hostside.app2.action.RECEIVER_READY";
- static final String ACTION_SHOW_TOAST =
- "com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST";
-
- protected static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
- protected static final String NOTIFICATION_TYPE_DELETE = "DELETE";
- protected static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
- protected static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
- protected static final String NOTIFICATION_TYPE_ACTION = "ACTION";
- protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
- protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
- private static final String NETWORK_STATUS_SEPARATOR = "\\|";
- private static final int SECOND_IN_MS = 1000;
- static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
-
- private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
- private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
- private static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
-
- private static final String EMPTY_STRING = "";
-
- protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
- protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
- protected static final int TYPE_EXPEDITED_JOB = 2;
-
- private static final int BATTERY_STATE_TIMEOUT_MS = 5000;
- private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500;
-
- private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 10_000;
- private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000;
- private static final int LAUNCH_ACTIVITY_TIMEOUT_MS = 10_000;
-
- // Must be higher than NETWORK_TIMEOUT_MS
- private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
-
- private static final IntentFilter BATTERY_CHANGED_FILTER =
- new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
-
- protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
-
- private static final long BROADCAST_TIMEOUT_MS = 5_000;
-
- protected Context mContext;
- protected Instrumentation mInstrumentation;
- protected ConnectivityManager mCm;
- protected int mUid;
- private int mMyUid;
- private @Nullable String mCustomUrl;
- private MyServiceClient mServiceClient;
- private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
- private PowerManager mPowerManager;
- private PowerManager.WakeLock mLock;
-
- @Rule
- public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
- .around(new MeterednessConfigurationRule());
-
- protected void setUp() throws Exception {
- mInstrumentation = getInstrumentation();
- mContext = getContext();
- mCm = getConnectivityManager();
- mDeviceIdleDeviceConfigStateHelper =
- new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_DEVICE_IDLE);
- mUid = getUid(TEST_APP2_PKG);
- mMyUid = getUid(mContext.getPackageName());
- mServiceClient = new MyServiceClient(mContext);
-
- final Bundle args = InstrumentationRegistry.getArguments();
- mCustomUrl = args.getString(ARG_CONNECTION_CHECK_CUSTOM_URL);
- if (mCustomUrl != null) {
- Log.d(TAG, "Using custom URL " + mCustomUrl + " for network checks");
- }
-
- 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
- // always be the case. Therefore, explicitly disable it before running the tests.
- // Invoke setRestrictBackgroundInternal() directly instead of going through
- // setRestrictBackground(), as some devices do not fully support the Data saver mode but
- // still have certain parts of it enabled by default.
- setRestrictBackgroundInternal(false);
- setAppIdle(false);
- mLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-
- Log.i(TAG, "Apps status:\n"
- + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n"
- + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid));
- }
-
- protected void tearDown() throws Exception {
- executeShellCommand("cmd netpolicy stop-watching");
- mServiceClient.unbind();
- final PowerManager.WakeLock lock = mLock;
- 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);
- }
-
- /**
- * Check if the flag to use different delays for sensitive 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_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN once the tests are moved to cts.
- */
- private boolean useDifferentDelaysForBackgroundChain() {
- if (!SdkLevel.isAtLeastV()) {
- return false;
- }
- final String output = executeShellCommand("device_config get backstage_power"
- + " com.android.server.net.use_different_delays_for_background_chain");
- return Boolean.parseBoolean(output);
- }
-
- protected int getUid(String packageName) throws Exception {
- return mContext.getPackageManager().getPackageUid(packageName, 0);
- }
-
- protected void assertRestrictBackgroundChangedReceived(int expectedCount) throws Exception {
- assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, expectedCount);
- assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
- }
-
- protected void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount)
- throws Exception {
- int attempts = 0;
- int count = 0;
- final int maxAttempts = 5;
- do {
- attempts++;
- count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
- assertFalse("Expected count " + expectedCount + " but actual is " + count,
- count > expectedCount);
- if (count == expectedCount) {
- break;
- }
- Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
- + attempts + " attempts; sleeping "
- + SLEEP_TIME_SEC + " seconds before trying again");
- // No sleep after the last turn
- if (attempts <= maxAttempts) {
- SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
- }
- } while (attempts <= maxAttempts);
- assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
- + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
- }
-
- protected void assertSnoozeWarningNotReceived() throws Exception {
- // Wait for a while to take broadcast queue delays into account
- SystemClock.sleep(BROADCAST_TIMEOUT_MS);
- assertEquals(0, getNumberBroadcastsReceived(DYNAMIC_RECEIVER, ACTION_SNOOZE_WARNING));
- }
-
- protected String sendOrderedBroadcast(Intent intent) throws Exception {
- return sendOrderedBroadcast(intent, ORDERED_BROADCAST_TIMEOUT_MS);
- }
-
- protected String sendOrderedBroadcast(Intent intent, int timeoutMs) throws Exception {
- final LinkedBlockingQueue<String> result = new LinkedBlockingQueue<>(1);
- Log.d(TAG, "Sending ordered broadcast: " + intent);
- mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final String resultData = getResultData();
- if (resultData == null) {
- Log.e(TAG, "Received null data from ordered intent");
- // Offer an empty string so that the code waiting for the result can return.
- result.offer(EMPTY_STRING);
- return;
- }
- result.offer(resultData);
- }
- }, null, 0, null, null);
-
- final String resultData = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
- Log.d(TAG, "Ordered broadcast response after " + timeoutMs + "ms: " + resultData );
- return resultData;
- }
-
- protected int getNumberBroadcastsReceived(String receiverName, String action) throws Exception {
- return mServiceClient.getCounters(receiverName, action);
- }
-
- protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception {
- final String status = mServiceClient.getRestrictBackgroundStatus();
- assertNotNull("didn't get API status from app2", status);
- assertEquals(restrictBackgroundValueToString(expectedStatus),
- restrictBackgroundValueToString(Integer.parseInt(status)));
- }
-
- /**
- * @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.
- */
- @Deprecated
- protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkAccess(expectAllowed, false, null);
- }
-
- protected void assertTopNetworkAccess(boolean expectAllowed) throws Exception {
- assertTopState();
- assertNetworkAccess(expectAllowed, true /* needScreenOn */);
- }
-
- protected void assertForegroundServiceNetworkAccess() throws Exception {
- assertForegroundServiceState();
- assertNetworkAccess(true /* expectAvailable */, false /* needScreenOn */);
- }
-
- /**
- * Asserts that an app always have access while on foreground or running a foreground service.
- *
- * <p>This method will launch an activity, a foreground service to make
- * the assertion, but will finish the activity / stop the service afterwards.
- */
- protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception{
- // Checks foreground first.
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- finishActivity();
-
- // Then foreground service
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- stopForegroundService();
- }
-
- protected void assertExpeditedJobHasNetworkAccess() throws Exception {
- launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB);
- finishExpeditedJob();
- }
-
- protected void assertExpeditedJobHasNoNetworkAccess() throws Exception {
- launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB, false);
- finishExpeditedJob();
- }
-
- /**
- * 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 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, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
- + ": " + state);
- if (statePredicate.test(state)) {
- return;
- }
- Log.i(TAG, "App not in desired process state on attempt #" + i
- + "; sleeping 1s before trying again");
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
- + " attempts: " + state);
- }
-
- /**
- * 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 void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
- throws Exception {
- if (expectAllowed && expectedUnavailableError != null) {
- throw new IllegalArgumentException("expectedUnavailableError is not null");
- }
- assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
- }
-
- /**
- * Asserts whether the active network is available or not.
- */
- private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
- throws Exception {
- assertNetworkAccess(expectAvailable, needScreenOn, null);
- }
-
- private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
- @Nullable final String expectedUnavailableError) throws Exception {
- final int maxTries = 5;
- String error = null;
- int timeoutMs = 500;
-
- for (int i = 1; i <= maxTries; i++) {
- error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
-
- if (error == null) return;
-
- // TODO: ideally, it should retry only when it cannot connect to an external site,
- // or no retry at all! But, currently, the initial change fails almost always on
- // battery saver tests because the netd changes are made asynchronously.
- // Once b/27803922 is fixed, this retry mechanism should be revisited.
-
- Log.w(TAG, "Network status didn't match for expectAvailable=" + expectAvailable
- + " on attempt #" + i + ": " + error + "\n"
- + "Sleeping " + timeoutMs + "ms before trying again");
- if (needScreenOn) {
- turnScreenOn();
- }
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(timeoutMs);
- }
- // Exponential back-off.
- timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
- }
- fail("Invalid state for " + mUid + "; expectAvailable=" + expectAvailable + " after "
- + maxTries + " attempts.\nLast error: " + error);
- }
-
- /**
- * Asserts whether the network is blocked by accessing bpf maps if command-line tool supports.
- */
- void assertNetworkAccessBlockedByBpf(boolean expectBlocked, int uid, boolean metered) {
- final String result;
- try {
- result = executeShellCommand(
- "cmd network_stack is-uid-networking-blocked " + uid + " " + metered);
- } catch (AssertionError e) {
- // If NetworkStack is too old to support this command, ignore and continue
- // this test to verify other parts.
- if (e.getMessage().contains("No shell command implementation.")) {
- return;
- }
- throw e;
- }
-
- // Tethering module is too old.
- if (result.contains("API is unsupported")) {
- return;
- }
-
- assertEquals(expectBlocked, parseBooleanOrThrow(result.trim()));
- }
-
- /**
- * Similar to {@link Boolean#parseBoolean} but throws when the input
- * is unexpected instead of returning false.
- */
- private static boolean parseBooleanOrThrow(@NonNull String s) {
- // Don't use Boolean.parseBoolean
- if ("true".equalsIgnoreCase(s)) return true;
- if ("false".equalsIgnoreCase(s)) return false;
- throw new IllegalArgumentException("Unexpected: " + s);
- }
-
- /**
- * Checks whether the network is available as expected.
- *
- * @return error message with the mismatch (or empty if assertion passed).
- */
- private String checkNetworkAccess(boolean expectAvailable,
- @Nullable final String expectedUnavailableError) throws Exception {
- final NetworkCheckResult checkResult = mServiceClient.checkNetworkStatus(mCustomUrl);
- return checkForAvailabilityInNetworkCheckResult(checkResult, expectAvailable,
- expectedUnavailableError);
- }
-
- private String checkForAvailabilityInNetworkCheckResult(NetworkCheckResult networkCheckResult,
- boolean expectAvailable, @Nullable final String expectedUnavailableError) {
- assertNotNull("NetworkCheckResult from app2 is null", networkCheckResult);
-
- final NetworkInfo networkInfo = networkCheckResult.networkInfo;
- assertNotNull("NetworkInfo from app2 is null", networkInfo);
-
- final State state = networkInfo.getState();
- final DetailedState detailedState = networkInfo.getDetailedState();
-
- final boolean connected = networkCheckResult.connected;
- final String connectionCheckDetails = networkCheckResult.details;
-
- final StringBuilder errors = new StringBuilder();
- final State expectedState;
- final DetailedState expectedDetailedState;
- if (expectAvailable) {
- expectedState = State.CONNECTED;
- expectedDetailedState = DetailedState.CONNECTED;
- } else {
- expectedState = State.DISCONNECTED;
- expectedDetailedState = DetailedState.BLOCKED;
- }
-
- if (expectAvailable != connected) {
- errors.append(String.format("External site connection failed: expected %s, got %s\n",
- expectAvailable, connected));
- }
- if (expectedState != state || expectedDetailedState != detailedState) {
- errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
- expectedState, expectedDetailedState, state, detailedState));
- } else if (!expectAvailable && (expectedUnavailableError != null)
- && !connectionCheckDetails.contains(expectedUnavailableError)) {
- errors.append("Connection unavailable reason mismatch: expected "
- + expectedUnavailableError + "\n");
- }
-
- if (errors.length() > 0) {
- errors.append("\tnetworkInfo: " + networkInfo + "\n");
- errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n");
- }
- return errors.length() == 0 ? null : errors.toString();
- }
-
- /**
- * Runs a Shell command which is not expected to generate output.
- */
- protected void executeSilentShellCommand(String command) {
- final String result = executeShellCommand(command);
- assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty());
- }
-
- /**
- * Asserts the result of a command, wait and re-running it a couple times if necessary.
- */
- protected void assertDelayedShellCommand(String command, final String expectedResult)
- throws Exception {
- assertDelayedShellCommand(command, 5, 1, expectedResult);
- }
-
- protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
- final String expectedResult) throws Exception {
- assertDelayedShellCommand(command, maxTries, napTimeSeconds, new ExpectResultChecker() {
-
- @Override
- public boolean isExpected(String result) {
- return expectedResult.equals(result);
- }
-
- @Override
- public String getExpected() {
- return expectedResult;
- }
- });
- }
-
- protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
- ExpectResultChecker checker) throws Exception {
- String result = "";
- for (int i = 1; i <= maxTries; i++) {
- result = executeShellCommand(command).trim();
- if (checker.isExpected(result)) return;
- Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
- + checker.getExpected() + "' on attempt #" + i
- + "; sleeping " + napTimeSeconds + "s before trying again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
- }
- }
- fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
- + maxTries
- + " attempts. Last result: '" + result + "'");
- }
-
- protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid);
- assertRestrictBackgroundWhitelist(uid, true);
- // UID policies live by the Highlander rule: "There can be only one".
- // Hence, if app is whitelisted, it should not be blacklisted.
- assertRestrictBackgroundBlacklist(uid, false);
- }
-
- protected void removeRestrictBackgroundWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy remove restrict-background-whitelist " + uid);
- assertRestrictBackgroundWhitelist(uid, false);
- }
-
- protected void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
- assertRestrictBackground("restrict-background-whitelist", uid, expected);
- }
-
- protected void addRestrictBackgroundBlacklist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy add restrict-background-blacklist " + uid);
- assertRestrictBackgroundBlacklist(uid, true);
- // UID policies live by the Highlander rule: "There can be only one".
- // Hence, if app is blacklisted, it should not be whitelisted.
- assertRestrictBackgroundWhitelist(uid, false);
- }
-
- protected void removeRestrictBackgroundBlacklist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy remove restrict-background-blacklist " + uid);
- assertRestrictBackgroundBlacklist(uid, false);
- }
-
- protected void assertRestrictBackgroundBlacklist(int uid, boolean expected) throws Exception {
- assertRestrictBackground("restrict-background-blacklist", uid, expected);
- }
-
- protected void addAppIdleWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy add app-idle-whitelist " + uid);
- assertAppIdleWhitelist(uid, true);
- }
-
- protected void removeAppIdleWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy remove app-idle-whitelist " + uid);
- assertAppIdleWhitelist(uid, false);
- }
-
- protected void assertAppIdleWhitelist(int uid, boolean expected) throws Exception {
- assertRestrictBackground("app-idle-whitelist", uid, expected);
- }
-
- private void assertRestrictBackground(String list, int uid, boolean expected) throws Exception {
- final int maxTries = 5;
- boolean actual = false;
- final String expectedUid = Integer.toString(uid);
- String uids = "";
- for (int i = 1; i <= maxTries; i++) {
- final String output =
- executeShellCommand("cmd netpolicy list " + list);
- uids = output.split(":")[1];
- for (String candidate : uids.split(" ")) {
- actual = candidate.trim().equals(expectedUid);
- if (expected == actual) {
- return;
- }
- }
- Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
- + expected + ", got " + actual + "); sleeping 1s before polling again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
- + ". Full list: " + uids);
- }
-
- protected void addTempPowerSaveModeWhitelist(String packageName, long duration)
- throws Exception {
- Log.i(TAG, "Adding pkg " + packageName + " to temp-power-save-mode whitelist");
- executeShellCommand("dumpsys deviceidle tempwhitelist -d " + duration + " " + packageName);
- }
-
- protected void assertPowerSaveModeWhitelist(String packageName, boolean expected)
- throws Exception {
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- assertDelayedShellCommand("dumpsys deviceidle whitelist =" + packageName,
- Boolean.toString(expected));
- }
-
- protected void addPowerSaveModeWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle whitelist +" + packageName);
- assertPowerSaveModeWhitelist(packageName, true);
- }
-
- protected void removePowerSaveModeWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Removing package " + packageName + " from power-save-mode whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle whitelist -" + packageName);
- assertPowerSaveModeWhitelist(packageName, false);
- }
-
- protected void assertPowerSaveModeExceptIdleWhitelist(String packageName, boolean expected)
- throws Exception {
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- assertDelayedShellCommand("dumpsys deviceidle except-idle-whitelist =" + packageName,
- Boolean.toString(expected));
- }
-
- protected void addPowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Adding package " + packageName + " to power-save-mode-except-idle whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle except-idle-whitelist +" + packageName);
- assertPowerSaveModeExceptIdleWhitelist(packageName, true);
- }
-
- protected void removePowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Removing package " + packageName
- + " from power-save-mode-except-idle whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle except-idle-whitelist reset");
- assertPowerSaveModeExceptIdleWhitelist(packageName, false);
- }
-
- protected void turnBatteryOn() throws Exception {
- executeSilentShellCommand("cmd battery unplug");
- executeSilentShellCommand("cmd battery set status "
- + BatteryManager.BATTERY_STATUS_DISCHARGING);
- assertBatteryState(false);
- }
-
- protected void turnBatteryOff() throws Exception {
- executeSilentShellCommand("cmd battery set ac " + BATTERY_PLUGGED_ANY);
- executeSilentShellCommand("cmd battery set level 100");
- executeSilentShellCommand("cmd battery set status "
- + BatteryManager.BATTERY_STATUS_CHARGING);
- assertBatteryState(true);
- }
-
- protected void resetBatteryState() {
- BatteryUtils.runDumpsysBatteryReset();
- }
-
- private void assertBatteryState(boolean pluggedIn) throws Exception {
- final long endTime = SystemClock.elapsedRealtime() + BATTERY_STATE_TIMEOUT_MS;
- while (isDevicePluggedIn() != pluggedIn && SystemClock.elapsedRealtime() <= endTime) {
- Thread.sleep(BATTERY_STATE_CHECK_INTERVAL_MS);
- }
- if (isDevicePluggedIn() != pluggedIn) {
- fail("Timed out waiting for the plugged-in state to change,"
- + " expected pluggedIn: " + pluggedIn);
- }
- }
-
- private boolean isDevicePluggedIn() {
- final Intent batteryIntent = mContext.registerReceiver(null, BATTERY_CHANGED_FILTER);
- return batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0;
- }
-
- protected void turnScreenOff() throws Exception {
- if (!mLock.isHeld()) mLock.acquire();
- executeSilentShellCommand("input keyevent KEYCODE_SLEEP");
- }
-
- protected void turnScreenOn() throws Exception {
- executeSilentShellCommand("input keyevent KEYCODE_WAKEUP");
- if (mLock.isHeld()) mLock.release();
- executeSilentShellCommand("wm dismiss-keyguard");
- }
-
- protected void setBatterySaverMode(boolean enabled) throws Exception {
- if (!isBatterySaverSupported()) {
- return;
- }
- Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
- if (enabled) {
- turnBatteryOn();
- AmUtils.waitForBroadcastBarrier();
- executeSilentShellCommand("cmd power set-mode 1");
- } else {
- executeSilentShellCommand("cmd power set-mode 0");
- turnBatteryOff();
- AmUtils.waitForBroadcastBarrier();
- }
- }
-
- protected void setDozeMode(boolean enabled) throws Exception {
- if (!isDozeModeSupported()) {
- return;
- }
-
- Log.i(TAG, "Setting Doze Mode to " + enabled);
- if (enabled) {
- turnBatteryOn();
- turnScreenOff();
- executeShellCommand("dumpsys deviceidle force-idle deep");
- } else {
- turnScreenOn();
- turnBatteryOff();
- executeShellCommand("dumpsys deviceidle unforce");
- }
- assertDozeMode(enabled);
- }
-
- protected void assertDozeMode(boolean enabled) throws Exception {
- assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
- }
-
- protected void stopApp() {
- executeSilentShellCommand("am stop-app " + TEST_APP2_PKG);
- }
-
- protected void setAppIdle(boolean isIdle) throws Exception {
- setAppIdleNoAssert(isIdle);
- assertAppIdle(isIdle);
- }
-
- protected void setAppIdleNoAssert(boolean isIdle) throws Exception {
- if (!isAppStandbySupported()) {
- return;
- }
- Log.i(TAG, "Setting app idle to " + isIdle);
- final String bucketName = isIdle ? "rare" : "active";
- executeSilentShellCommand("am set-standby-bucket " + TEST_APP2_PKG + " " + bucketName);
- }
-
- protected void assertAppIdle(boolean isIdle) throws Exception {
- try {
- assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
- 30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + isIdle);
- } catch (Throwable e) {
- throw e;
- }
- }
-
- /**
- * Starts a service that will register a broadcast receiver to receive
- * {@code RESTRICT_BACKGROUND_CHANGE} intents.
- * <p>
- * The service must run in a separate app because otherwise it would be killed every time
- * {@link #runDeviceTests(String, String)} is executed.
- */
- protected void registerBroadcastReceiver() throws Exception {
- mServiceClient.registerBroadcastReceiver();
-
- final Intent intent = new Intent(ACTION_RECEIVER_READY)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- // Wait until receiver is ready.
- final int maxTries = 10;
- for (int i = 1; i <= maxTries; i++) {
- final String message = sendOrderedBroadcast(intent, SECOND_IN_MS * 4);
- Log.d(TAG, "app2 receiver acked: " + message);
- if (message != null) {
- return;
- }
- Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("app2 receiver is not ready in " + mUid);
- }
-
- protected void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
- throws Exception {
- Log.i(TAG, "Registering network callback for request: " + request);
- mServiceClient.registerNetworkCallback(request, cb);
- }
-
- protected void unregisterNetworkCallback() throws Exception {
- mServiceClient.unregisterNetworkCallback();
- }
-
- /**
- * Registers a {@link NotificationListenerService} implementation that will execute the
- * notification actions right after the notification is sent.
- */
- protected void registerNotificationListenerService() throws Exception {
- executeShellCommand("cmd notification allow_listener "
- + MyNotificationListenerService.getId());
- final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
- final ComponentName listenerComponent = MyNotificationListenerService.getComponentName();
- assertTrue(listenerComponent + " has not been granted access",
- nm.isNotificationListenerAccessGranted(listenerComponent));
- }
-
- protected void setPendingIntentAllowlistDuration(long durationMs) {
- mDeviceIdleDeviceConfigStateHelper.set("notification_allowlist_duration_ms",
- String.valueOf(durationMs));
- }
-
- protected void resetDeviceIdleSettings() {
- mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues();
- }
-
- protected void launchActivity() throws Exception {
- turnScreenOn();
- final CountDownLatch latch = new CountDownLatch(1);
- final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
- final RemoteCallback callback = new RemoteCallback(result -> latch.countDown());
- launchIntent.putExtra(Intent.EXTRA_REMOTE_CALLBACK, callback);
- mContext.startActivity(launchIntent);
- // There might be a race when app2 is launched but ACTION_FINISH_ACTIVITY has not registered
- // before test calls finishActivity(). When the issue is happened, there is no way to fix
- // it, so have a callback design to make sure that the app is launched completely and
- // ACTION_FINISH_ACTIVITY will be registered before leaving this method.
- if (!latch.await(LAUNCH_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- fail("Timed out waiting for launching activity");
- }
- }
-
- protected void launchComponentAndAssertNetworkAccess(int type) throws Exception {
- launchComponentAndAssertNetworkAccess(type, true);
- }
-
- protected void launchComponentAndAssertNetworkAccess(int type, boolean expectAvailable)
- throws Exception {
- if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
- startForegroundService();
- assertForegroundServiceNetworkAccess();
- } else if (type == TYPE_COMPONENT_ACTIVTIY) {
- turnScreenOn();
- final CountDownLatch latch = new CountDownLatch(1);
- final Intent launchIntent = getIntentForComponent(type);
- final Bundle extras = new Bundle();
- final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
- new AtomicReference<>();
- extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
- extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
- extras.putString(KEY_CUSTOM_URL, mCustomUrl);
- launchIntent.putExtras(extras);
- mContext.startActivity(launchIntent);
- if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- final int resultCode = result.get().first;
- final NetworkCheckResult networkCheckResult = result.get().second;
- if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
- final String error = checkForAvailabilityInNetworkCheckResult(
- networkCheckResult, expectAvailable,
- null /* expectedUnavailableError */);
- if (error != null) {
- fail("Network is not available for activity in app2 (" + mUid + "): "
- + error);
- }
- } else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
- Log.d(TAG, networkCheckResult.details);
- // App didn't come to foreground when the activity is started, so try again.
- assertTopNetworkAccess(true);
- } else {
- fail("Unexpected resultCode=" + resultCode
- + "; networkCheckResult=[" + networkCheckResult + "]");
- }
- } else {
- fail("Timed out waiting for network availability status from app2's activity ("
- + mUid + ")");
- }
- } else if (type == TYPE_EXPEDITED_JOB) {
- final Bundle extras = new Bundle();
- final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
- new AtomicReference<>();
- final CountDownLatch latch = new CountDownLatch(1);
- extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
- extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
- extras.putString(KEY_CUSTOM_URL, mCustomUrl);
- final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
- .setExpedited(true)
- .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
- .setTransientExtras(extras)
- .build();
- assertEquals("Error scheduling " + jobInfo,
- RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
- forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
- if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- final int resultCode = result.get().first;
- final NetworkCheckResult networkCheckResult = result.get().second;
- if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
- final String error = checkForAvailabilityInNetworkCheckResult(
- networkCheckResult, expectAvailable,
- null /* expectedUnavailableError */);
- if (error != null) {
- Log.d(TAG, "Network state is unexpected, checking again. " + error);
- // Right now we could end up in an unexpected state if expedited job
- // doesn't have network access immediately after starting, so check again.
- assertNetworkAccess(expectAvailable, false /* needScreenOn */);
- }
- } else {
- fail("Unexpected resultCode=" + resultCode
- + "; networkCheckResult=[" + networkCheckResult + "]");
- }
- } else {
- fail("Timed out waiting for network availability status from app2's expedited job ("
- + mUid + ")");
- }
- } else {
- throw new IllegalArgumentException("Unknown type: " + type);
- }
- }
-
- protected void startActivity() throws Exception {
- final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
- mContext.startActivity(launchIntent);
- }
-
- private void startForegroundService() throws Exception {
- final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE);
- mContext.startForegroundService(launchIntent);
- assertForegroundServiceState();
- }
-
- private Intent getIntentForComponent(int type) {
- final Intent intent = new Intent();
- if (type == TYPE_COMPONENT_ACTIVTIY) {
- intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
- intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
- .setFlags(1);
- } else {
- fail("Unknown type: " + type);
- }
- return intent;
- }
-
- protected void stopForegroundService() throws Exception {
- executeShellCommand(String.format("am startservice -f 2 %s/%s",
- TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS));
- // NOTE: cannot assert state because it depends on whether activity was on top before.
- }
-
- private Binder getNewNetworkStateObserver(final CountDownLatch latch,
- final AtomicReference<Pair<Integer, NetworkCheckResult>> result) {
- return new INetworkStateObserver.Stub() {
- @Override
- public void onNetworkStateChecked(int resultCode,
- NetworkCheckResult networkCheckResult) {
- result.set(Pair.create(resultCode, networkCheckResult));
- latch.countDown();
- }
- };
- }
-
- /**
- * Finishes an activity on app2 so its process is demoted from foreground status.
- */
- protected void finishActivity() throws Exception {
- final Intent intent = new Intent(ACTION_FINISH_ACTIVITY)
- .setPackage(TEST_APP2_PKG)
- .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- sendOrderedBroadcast(intent);
- }
-
- /**
- * Finishes the expedited job on app2 so its process is demoted from foreground status.
- */
- private void finishExpeditedJob() throws Exception {
- final Intent intent = new Intent(ACTION_FINISH_JOB)
- .setPackage(TEST_APP2_PKG)
- .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- sendOrderedBroadcast(intent);
- }
-
- protected void sendNotification(int notificationId, String notificationType) throws Exception {
- Log.d(TAG, "Sending notification broadcast (id=" + notificationId
- + ", type=" + notificationType);
- mServiceClient.sendNotification(notificationId, notificationType);
- }
-
- protected String showToast() {
- final Intent intent = new Intent(ACTION_SHOW_TOAST);
- intent.setPackage(TEST_APP2_PKG);
- Log.d(TAG, "Sending request to show toast");
- try {
- return sendOrderedBroadcast(intent, 3 * SECOND_IN_MS);
- } catch (Exception e) {
- return "";
- }
- }
-
- private ProcessState getProcessStateByUid(int uid) throws Exception {
- return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid));
- }
-
- private static class ProcessState {
- private final String fullState;
- final int state;
-
- ProcessState(String fullState) {
- this.fullState = fullState;
- try {
- this.state = Integer.parseInt(fullState.split(" ")[0]);
- } catch (Exception e) {
- throw new IllegalArgumentException("Could not parse " + fullState);
- }
- }
-
- @Override
- public String toString() {
- return fullState;
- }
- }
-
- /**
- * Helper class used to assert the result of a Shell command.
- */
- protected static interface ExpectResultChecker {
- /**
- * Checkes whether the result of the command matched the expectation.
- */
- boolean isExpected(String result);
- /**
- * Gets the expected result so it's displayed on log and failure messages.
- */
- String getExpected();
- }
-
- protected void setRestrictedNetworkingMode(boolean enabled) throws Exception {
- executeSilentShellCommand(
- "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
- assertRestrictedNetworkingModeState(enabled);
- }
-
- protected void assertRestrictedNetworkingModeState(boolean enabled) throws Exception {
- assertDelayedShellCommand("cmd netpolicy get restricted-mode",
- "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
deleted file mode 100644
index 6b802f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
+++ /dev/null
@@ -1,23 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
deleted file mode 100644
index 2e725ae..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
+++ /dev/null
@@ -1,23 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
deleted file mode 100644
index 2e421f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
+++ /dev/null
@@ -1,23 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
deleted file mode 100644
index 0be5644..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
+++ /dev/null
@@ -1,24 +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.cts.netpolicy.hostside;
-
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
deleted file mode 100644
index bfccce9..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
+++ /dev/null
@@ -1,129 +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.cts.netpolicy.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.netpolicy.hostside.NetworkPolicyTestUtils.getUiDevice;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.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;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
- private static final int TEST_ITERATION_COUNT = 5;
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
- resetDeviceState();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
- stopApp();
- resetDeviceState();
- }
-
- private void resetDeviceState() throws Exception {
- resetBatteryState();
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setAppIdle(false);
- setDozeMode(false);
- }
-
-
- @Test
- @RequiredProperties({BATTERY_SAVER_MODE})
- public void testStartActivity_batterySaver() throws Exception {
- setBatterySaverMode(true);
- assertNetworkAccess(false, null);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
- public void testStartActivity_dataSaver() throws Exception {
- setRestrictBackground(true);
- assertNetworkAccess(false, null);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
- }
-
- @Test
- @RequiredProperties({DOZE_MODE})
- public void testStartActivity_doze() throws Exception {
- setDozeMode(true);
- assertNetworkAccess(false, null);
- // TODO (235284115): We need to turn on Doze every time before starting
- // the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
- }
-
- @Test
- @RequiredProperties({APP_STANDBY_MODE})
- public void testStartActivity_appStandby() throws Exception {
- turnBatteryOn();
- setAppIdle(true);
- assertNetworkAccess(false, null);
- // TODO (235284115): We need to put the app into app standby mode every
- // time before starting the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
- }
-
- @Test
- public void testStartActivity_default() throws Exception {
- assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
- assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- SystemClock.sleep(mProcessStateTransitionLongDelayMs);
- assertNetworkAccess(false, null);
- });
- }
-
- 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();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- Log.i(TAG, testName + " end #" + i);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
deleted file mode 100644
index 66e0d00..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
+++ /dev/null
@@ -1,213 +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.cts.netpolicy.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-
-import static com.android.compatibility.common.util.FeatureUtil.isTV;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NO_DATA_SAVER_MODE;
-
-import static org.junit.Assert.fail;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.compatibility.common.util.CddTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-@LargeTest
-public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
-
- private static final String[] REQUIRED_WHITELISTED_PACKAGES = {
- "com.android.providers.downloads"
- };
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- setRestrictBackground(false);
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
-
- registerBroadcastReceiver();
- assertRestrictBackgroundChangedReceived(0);
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- setRestrictBackground(false);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_disabled() throws Exception {
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
- // Verify status is always disabled, never whitelisted
- addRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(0);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_whitelisted() throws Exception {
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- addRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(2);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
- removeRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(3);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_enabled() throws Exception {
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- // Make sure foreground app doesn't lose access upon enabling Data Saver.
- setRestrictBackground(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- setRestrictBackground(true);
- assertTopNetworkAccess(true);
-
- // Although it should not have access while the screen is off.
- turnScreenOff();
- assertBackgroundNetworkAccess(false);
- turnScreenOn();
- // On some TVs, it is possible that the activity on top may change after the screen is
- // turned off and on again, so relaunch the activity in the test app again.
- if (isTV()) {
- startActivity();
- }
- assertTopNetworkAccess(true);
-
- // Goes back to background state.
- finishActivity();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground service doesn't lose access upon enabling Data Saver.
- setRestrictBackground(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setRestrictBackground(true);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_blacklisted() throws Exception {
- addRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- // UID policies live by the Highlander rule: "There can be only one".
- // Hence, if app is whitelisted, it should not be blacklisted anymore.
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(2);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- addRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(3);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
- // Check status after removing blacklist.
- // ...re-enables first
- addRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(4);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- assertsForegroundAlwaysHasNetworkAccess();
- // ... remove blacklist - access's still rejected because Data Saver is on
- removeRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(4);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- assertsForegroundAlwaysHasNetworkAccess();
- // ... finally, disable Data Saver
- setRestrictBackground(false);
- assertRestrictBackgroundChangedReceived(5);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
- assertsForegroundAlwaysHasNetworkAccess();
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception {
- final StringBuilder error = new StringBuilder();
- for (String packageName : REQUIRED_WHITELISTED_PACKAGES) {
- int uid = -1;
- try {
- uid = getUid(packageName);
- assertRestrictBackgroundWhitelist(uid, true);
- } catch (Throwable t) {
- error.append("\nFailed for '").append(packageName).append("'");
- if (uid > 0) {
- error.append(" (uid ").append(uid).append(")");
- }
- error.append(": ").append(t).append("\n");
- }
- }
- if (error.length() > 0) {
- fail(error.toString());
- }
- }
-
- @RequiredProperties({NO_DATA_SAVER_MODE})
- @CddTest(requirement="7.4.7/C-2-2")
- @Test
- public void testBroadcastNotSentOnUnsupportedDevices() throws Exception {
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(0);
-
- setRestrictBackground(false);
- assertRestrictBackgroundChangedReceived(0);
-
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(0);
- }
-
- private void assertDataSaverStatusOnBackground(int expectedStatus) throws Exception {
- assertRestrictBackgroundStatus(expectedStatus);
- assertBackgroundNetworkAccess(expectedStatus != RESTRICT_BACKGROUND_STATUS_ENABLED);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
deleted file mode 100644
index 69ca206..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.clearSnoozeTimestamps;
-
-import android.content.pm.PackageManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.SubscriptionPlan;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.Direction;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
-
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.UiAutomatorUtils2;
-
-import org.junit.After;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.time.Period;
-import java.time.ZonedDateTime;
-import java.util.Arrays;
-import java.util.List;
-
-public class DataWarningReceiverTest extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- clearSnoozeTimestamps();
- registerBroadcastReceiver();
- turnScreenOn();
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
- }
-
- @Test
- public void testSnoozeWarningNotReceived() throws Exception {
- Assume.assumeTrue("Feature not supported: " + PackageManager.FEATURE_TELEPHONY,
- mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
- final SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
- final int subId = SubscriptionManager.getDefaultDataSubscriptionId();
- Assume.assumeTrue("Valid subId not found",
- subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-
- setSubPlanOwner(subId, TEST_PKG);
- final List<SubscriptionPlan> originalPlans = sm.getSubscriptionPlans(subId);
- try {
- // In NetworkPolicyManagerService class, we set the data warning bytes to 90% of
- // data limit bytes. So, create the subscription plan in such a way this data warning
- // threshold is already reached.
- final SubscriptionPlan plan = SubscriptionPlan.Builder
- .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
- Period.ofMonths(1))
- .setTitle("CTS")
- .setDataLimit(1_000_000_000, SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
- .setDataUsage(999_000_000, System.currentTimeMillis())
- .build();
- sm.setSubscriptionPlans(subId, Arrays.asList(plan));
- final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
- uiDevice.openNotification();
- try {
- final UiObject2 uiObject = UiAutomatorUtils2.waitFindObject(
- By.text("Data warning"));
- Assume.assumeNotNull(uiObject);
- uiObject.wait(Until.clickable(true), 10_000L);
- uiObject.getParent().swipe(Direction.RIGHT, 1.0f);
- } catch (Throwable t) {
- Assume.assumeNoException(
- "Error occurred while finding and swiping the notification", t);
- }
- assertSnoozeWarningNotReceived();
- uiDevice.pressHome();
- } finally {
- sm.setSubscriptionPlans(subId, originalPlans);
- setSubPlanOwner(subId, "");
- }
- }
-
- private static void setSubPlanOwner(int subId, String packageName) throws Exception {
- SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
- "cmd netpolicy set sub-plan-owner " + subId + " " + packageName);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
deleted file mode 100644
index 810fd19..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
deleted file mode 100644
index fef546c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
deleted file mode 100644
index 741dd7e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
+++ /dev/null
@@ -1,23 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
deleted file mode 100644
index 2dc6cc4..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_PKG;
-
-import android.os.Environment;
-import android.os.FileUtils;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.OnFailureRule;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-public class DumpOnFailureRule extends OnFailureRule {
- private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
- "CtsHostsideNetworkPolicyTests");
-
- @Override
- public void onTestFailure(Statement base, Description description, Throwable throwable) {
- if (throwable instanceof AssumptionViolatedException) {
- final String testName = description.getClassName() + "_" + description.getMethodName();
- Log.d(TAG, "Skipping test " + testName + ": " + throwable);
- return;
- }
-
- prepareDumpRootDir();
- final String shortenedTestName = getShortenedTestName(description);
- final File dumpFile = new File(mDumpDir, "dump-" + shortenedTestName);
- Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
- try (FileOutputStream out = new FileOutputStream(dumpFile)) {
- for (String cmd : new String[] {
- "dumpsys netpolicy",
- "dumpsys network_management",
- "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG,
- "dumpsys usagestats appstandby",
- "dumpsys connectivity trafficcontroller",
- "dumpsys netd trafficcontroller",
- "dumpsys platform_compat", // TODO (b/279829773): Remove this dump
- "dumpsys jobscheduler " + TEST_APP2_PKG, // TODO (b/288220398): Remove this dump
- }) {
- dumpCommandOutput(out, cmd);
- }
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Error opening file: " + dumpFile, e);
- } catch (IOException e) {
- Log.e(TAG, "Error closing file: " + dumpFile, e);
- }
- final UiDevice uiDevice = UiDevice.getInstance(
- InstrumentationRegistry.getInstrumentation());
- final File screenshotFile = new File(mDumpDir, "sc-" + shortenedTestName + ".png");
- uiDevice.takeScreenshot(screenshotFile);
- final File windowHierarchyFile = new File(mDumpDir, "wh-" + shortenedTestName + ".xml");
- try {
- uiDevice.dumpWindowHierarchy(windowHierarchyFile);
- } catch (IOException e) {
- Log.e(TAG, "Error dumping window hierarchy", e);
- }
- }
-
- private String getShortenedTestName(Description description) {
- final String qualifiedClassName = description.getClassName();
- final String className = qualifiedClassName.substring(
- qualifiedClassName.lastIndexOf(".") + 1);
- final String shortenedClassName = className.chars()
- .filter(Character::isUpperCase)
- .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
- .toString();
- return shortenedClassName + "_" + description.getMethodName();
- }
-
- void dumpCommandOutput(FileOutputStream out, String cmd) {
- final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
- .getUiAutomation().executeShellCommand(cmd);
- try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
- out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8));
- FileUtils.copy(in, out);
- out.write("\n\n=================================================================\n\n"
- .getBytes(StandardCharsets.UTF_8));
- } catch (IOException e) {
- Log.e(TAG, "Error dumping '" + cmd + "'", e);
- }
- }
-
- void prepareDumpRootDir() {
- if (!mDumpDir.exists() && !mDumpDir.mkdir()) {
- Log.e(TAG, "Error creating " + mDumpDir);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
deleted file mode 100644
index d56a50b..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
deleted file mode 100644
index 0a776ee..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
deleted file mode 100644
index 4f4e68e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import android.util.ArraySet;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class MeterednessConfigurationRule extends BeforeAfterRule {
- private ThrowingRunnable mMeterednessResetter;
-
- @Override
- public void onBefore(Statement base, Description description) throws Throwable {
- final ArraySet<Property> requiredProperties
- = RequiredPropertiesRule.getRequiredProperties();
- if (requiredProperties.contains(METERED_NETWORK)) {
- configureNetworkMeteredness(true);
- } else if (requiredProperties.contains(NON_METERED_NETWORK)) {
- configureNetworkMeteredness(false);
- }
- }
-
- @Override
- public void onAfter(Statement base, Description description) throws Throwable {
- resetNetworkMeteredness();
- }
-
- public void configureNetworkMeteredness(boolean metered) throws Exception {
- mMeterednessResetter = setupActiveNetworkMeteredness(metered);
- }
-
- public void resetNetworkMeteredness() throws Exception {
- if (mMeterednessResetter != null) {
- mMeterednessResetter.run();
- mMeterednessResetter = null;
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
deleted file mode 100644
index b0fa106..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
+++ /dev/null
@@ -1,370 +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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Test cases for the more complex scenarios where multiple restrictions (like Battery Saver Mode
- * and Data Saver Mode) are applied simultaneously.
- * <p>
- * <strong>NOTE: </strong>it might sound like the test methods on this class are testing too much,
- * which would make it harder to diagnose individual failures, but the assumption is that such
- * failure most likely will happen when the restriction is tested individually as well.
- */
-public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase {
- private static final String TAG = "MixedModesTest";
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
- registerBroadcastReceiver();
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- try {
- setRestrictBackground(false);
- } finally {
- setBatterySaverMode(false);
- }
- }
-
- /**
- * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on metered networks.
- */
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
- @Test
- public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
- final MeterednessConfigurationRule meterednessConfiguration
- = new MeterednessConfigurationRule();
- meterednessConfiguration.configureNetworkMeteredness(true);
- try {
- setRestrictBackground(true);
- setBatterySaverMode(true);
-
- Log.v(TAG, "Not whitelisted for any.");
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
- addRestrictBackgroundWhitelist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- removeRestrictBackgroundWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
- Log.v(TAG, "Whitelisted for both.");
- addRestrictBackgroundWhitelist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundBlacklist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- } finally {
- meterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- /**
- * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on non-metered
- * networks.
- */
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, NON_METERED_NETWORK})
- @Test
- public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
- final MeterednessConfigurationRule meterednessConfiguration
- = new MeterednessConfigurationRule();
- meterednessConfiguration.configureNetworkMeteredness(false);
- try {
- setRestrictBackground(true);
- setBatterySaverMode(true);
-
- Log.v(TAG, "Not whitelisted for any.");
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
- addRestrictBackgroundWhitelist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- removeRestrictBackgroundWhitelist(mUid);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
- Log.v(TAG, "Whitelisted for both.");
- addRestrictBackgroundWhitelist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundBlacklist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removeRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- } finally {
- meterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- /**
- * Tests that powersave whitelists works as expected when doze and battery saver modes
- * are enabled.
- */
- @RequiredProperties({DOZE_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
- setBatterySaverMode(true);
- setDozeMode(true);
-
- try {
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- } finally {
- setBatterySaverMode(false);
- setDozeMode(false);
- }
- }
-
- /**
- * Tests that powersave whitelists works as expected when doze and appIdle modes
- * are enabled.
- */
- @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
- @Test
- public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
- @Test
- public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
- setBatterySaverMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setBatterySaverMode(false);
- }
- }
-
- /**
- * Tests that the app idle whitelist works as expected when doze and appIdle mode are enabled.
- */
- @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
- @Test
- public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- // UID still shouldn't have access because of Doze.
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- removeAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
- @Test
- public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- removeAppIdleWhitelist(mUid);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- setBatterySaverMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setBatterySaverMode(false);
- removeAppIdleWhitelist(mUid);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
deleted file mode 100644
index 6dc9921..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
+++ /dev/null
@@ -1,123 +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.cts.netpolicy.hostside;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
-import android.app.RemoteInput;
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.service.notification.NotificationListenerService;
-import android.service.notification.StatusBarNotification;
-import android.util.Log;
-
-/**
- * NotificationListenerService implementation that executes the notification actions once they're
- * created.
- */
-public class MyNotificationListenerService extends NotificationListenerService {
- private static final String TAG = "MyNotificationListenerService";
-
- @Override
- public void onListenerConnected() {
- Log.d(TAG, "onListenerConnected()");
- }
-
- @Override
- public void onNotificationPosted(StatusBarNotification sbn) {
- Log.d(TAG, "onNotificationPosted(): " + sbn);
- if (!sbn.getPackageName().startsWith(getPackageName())) {
- Log.v(TAG, "ignoring notification from a different package");
- return;
- }
- final PendingIntentSender sender = new PendingIntentSender();
- final Notification notification = sbn.getNotification();
- if (notification.contentIntent != null) {
- sender.send("content", notification.contentIntent);
- }
- if (notification.deleteIntent != null) {
- sender.send("delete", notification.deleteIntent);
- }
- if (notification.fullScreenIntent != null) {
- sender.send("full screen", notification.fullScreenIntent);
- }
- if (notification.actions != null) {
- for (Notification.Action action : notification.actions) {
- sender.send("action", action.actionIntent);
- sender.send("action extras", action.getExtras());
- final RemoteInput[] remoteInputs = action.getRemoteInputs();
- if (remoteInputs != null && remoteInputs.length > 0) {
- for (RemoteInput remoteInput : remoteInputs) {
- sender.send("remote input extras", remoteInput.getExtras());
- }
- }
- }
- }
- sender.send("notification extras", notification.extras);
- }
-
- static String getId() {
- return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(),
- MyNotificationListenerService.class.getName());
- }
-
- static ComponentName getComponentName() {
- return new ComponentName(MyNotificationListenerService.class.getPackage().getName(),
- MyNotificationListenerService.class.getName());
- }
-
- private static final class PendingIntentSender {
- private PendingIntent mSentIntent = null;
- private String mReason = null;
-
- private void send(String reason, PendingIntent pendingIntent) {
- if (pendingIntent == null) {
- // Could happen on action that only has extras
- Log.v(TAG, "Not sending null pending intent for " + reason);
- return;
- }
- if (mSentIntent != null || mReason != null) {
- // Sanity check: make sure test case set up just one pending intent in the
- // notification, otherwise it could pass because another pending intent caused the
- // whitelisting.
- throw new IllegalStateException("Already sent a PendingIntent (" + mSentIntent
- + ") for reason '" + mReason + "' when requested another for '" + reason
- + "' (" + pendingIntent + ")");
- }
- Log.i(TAG, "Sending pending intent for " + reason + ":" + pendingIntent);
- try {
- pendingIntent.send();
- mSentIntent = pendingIntent;
- mReason = reason;
- } catch (CanceledException e) {
- Log.w(TAG, "Pending intent " + pendingIntent + " canceled");
- }
- }
-
- private void send(String reason, Bundle extras) {
- if (extras != null) {
- for (String key : extras.keySet()) {
- Object value = extras.get(key);
- if (value instanceof PendingIntent) {
- send(reason + " with key '" + key + "'", (PendingIntent) value);
- }
- }
- }
- }
-
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
deleted file mode 100644
index 71b28f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
+++ /dev/null
@@ -1,133 +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.cts.netpolicy.hostside;
-
-import android.app.job.JobInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.net.NetworkRequest;
-import android.os.ConditionVariable;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-public class MyServiceClient {
- private static final int TIMEOUT_MS = 20_000;
- private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
- private static final String APP2_PACKAGE = PACKAGE + ".app2";
- private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
-
- private Context mContext;
- private ServiceConnection mServiceConnection;
- private volatile IMyService mService;
- private final ConditionVariable mServiceCondition = new ConditionVariable();
-
- public MyServiceClient(Context context) {
- mContext = context;
- }
-
- /**
- * 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");
- }
- mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mService = IMyService.Stub.asInterface(service);
- mServiceCondition.open();
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- mServiceCondition.close();
- mService = null;
- }
- };
-
- final Intent intent = new Intent();
- intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
- // 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
- | bindPriorityFlags);
- ensureServiceConnection();
- }
-
- public void unbind() {
- if (mService != null) {
- mContext.unbindService(mServiceConnection);
- }
- }
-
- 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);
- }
-
- /** Retrieves the network state as observed from the bound test app */
- public NetworkCheckResult checkNetworkStatus(String address) throws RemoteException {
- ensureServiceConnection();
- return mService.checkNetworkStatus(address);
- }
-
- 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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
deleted file mode 100644
index 3934cfa..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.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;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.net.Network;
-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;
-
-import org.junit.After;
-import org.junit.Before;
-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;
-
-public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
- private Network mNetwork;
- private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
- private CtsNetUtils mCtsNetUtils;
- private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
-
- @Rule
- public final MeterednessConfigurationRule mMeterednessConfiguration
- = new MeterednessConfigurationRule();
-
- enum CallbackState {
- NONE,
- AVAILABLE,
- LOST,
- BLOCKED_STATUS,
- CAPABILITIES
- }
-
- private static class CallbackInfo {
- public final CallbackState state;
- public final Network network;
- public final Object arg;
-
- CallbackInfo(CallbackState s, Network n, Object o) {
- state = s; network = n; arg = o;
- }
-
- public String toString() {
- return String.format("%s (%s) (%s)", state, network, arg);
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof CallbackInfo)) return false;
- // Ignore timeMs, since it's unpredictable.
- final CallbackInfo other = (CallbackInfo) o;
- return (state == other.state) && Objects.equals(network, other.network)
- && Objects.equals(arg, other.arg);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(state, network, arg);
- }
- }
-
- private class TestNetworkCallback extends INetworkCallback.Stub {
- private static final int TEST_CONNECT_TIMEOUT_MS = 30_000;
- private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000;
-
- private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
-
- protected void setLastCallback(CallbackState state, Network network, Object o) {
- mCallbacks.offer(new CallbackInfo(state, network, o));
- }
-
- CallbackInfo nextCallback(int timeoutMs) {
- CallbackInfo cb = null;
- try {
- cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- }
- if (cb == null) {
- fail("Did not receive callback after " + timeoutMs + "ms");
- }
- return cb;
- }
-
- CallbackInfo expectCallback(CallbackState state, Network expectedNetwork, Object o) {
- final CallbackInfo expected = new CallbackInfo(state, expectedNetwork, o);
- final CallbackInfo actual = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
- assertEquals("Unexpected callback:", expected, actual);
- return actual;
- }
-
- @Override
- public void onAvailable(Network network) {
- setLastCallback(CallbackState.AVAILABLE, network, null);
- }
-
- @Override
- public void onLost(Network network) {
- setLastCallback(CallbackState.LOST, network, null);
- }
-
- @Override
- public void onBlockedStatusChanged(Network network, boolean blocked) {
- setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked);
- }
-
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
- setLastCallback(CallbackState.CAPABILITIES, network, cap);
- }
-
- 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);
- }
- 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);
- }
-
- public void expectBlockedStatusCallbackEventually(Network expectedNetwork,
- boolean expectBlocked) {
- final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
- do {
- final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
- if (cb.state == CallbackState.BLOCKED_STATUS
- && cb.network.equals(expectedNetwork)) {
- assertEquals(expectBlocked, cb.arg);
- return;
- }
- } while (System.currentTimeMillis() <= deadline);
- fail("Didn't receive onBlockedStatusChanged()");
- }
-
- public void expectCapabilitiesCallbackEventually(Network expectedNetwork, boolean hasCap,
- int cap) {
- final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
- do {
- final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
- if (cb.state != CallbackState.CAPABILITIES
- || !expectedNetwork.equals(cb.network)
- || (hasCap != ((NetworkCapabilities) cb.arg).hasCapability(cap))) {
- Log.i("NetworkCallbackTest#expectCapabilitiesCallback",
- "Ignoring non-matching callback : " + cb);
- continue;
- }
- // Found a match, return
- return;
- } while (System.currentTimeMillis() <= deadline);
- fail("Didn't receive the expected callback to onCapabilitiesChanged(). Check the "
- + "log for a list of received callbacks, if any.");
- }
- }
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- assumeTrue(canChangeActiveNetworkMeteredness());
-
- registerBroadcastReceiver();
-
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(0);
-
- // 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.
- final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-
- // Mark network as metered.
- mMeterednessConfiguration.configureNetworkMeteredness(true);
-
- // Register callback, copy the capabilities from the active network to expect the "original"
- // network before disconnecting, but null out some fields to prevent over-specified.
- registerNetworkCallback(new NetworkRequest.Builder()
- .setCapabilities(networkCapabilities.setTransportInfo(null))
- .removeCapability(NET_CAPABILITY_NOT_METERED)
- .setSignalStrength(SIGNAL_STRENGTH_UNSPECIFIED).build(), mTestNetworkCallback);
- // Wait for onAvailable() callback to ensure network is available before the test
- // and store the default network.
- mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
- // Check that the network is metered.
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- 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
- // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
- // starting from Android T. But for Data Saver, the fix is not backward compatible since
- // there are some platform changes involved. It is only available on devices that a specific
- // trunk flag is enabled.
- //
- // This test can not only verify that the network traffic from apps is blocked at the right
- // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
- // socket connection stage.
- if (SdkLevel.isAtLeastT()) {
- // Enable private DNS
- mCtsNetUtils = new CtsNetUtils(mContext);
- mCtsNetUtils.storePrivateDnsSetting();
- mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
- mCtsNetUtils.awaitPrivateDnsSetting(
- "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
- GOOGLE_PRIVATE_DNS_SERVER, true);
- }
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- setRestrictBackground(false);
- setBatterySaverMode(false);
- unregisterNetworkCallback();
- stopApp();
-
- if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
- mCtsNetUtils.restorePrivateDnsSetting();
- }
- }
-
- @RequiredProperties({DATA_SAVER_MODE})
- @Test
- public void testOnBlockedStatusChanged_dataSaver() throws Exception {
- try {
- // Enable restrict background
- setRestrictBackground(true);
- // TODO: Verify expectedUnavailableError when aconfig support mainline.
- // (see go/aconfig-in-mainline-problems)
- assertBackgroundNetworkAccess(false);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
- // Add to whitelist
- addRestrictBackgroundWhitelist(mUid);
- assertBackgroundNetworkAccess(true);
- assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
- // Remove from whitelist
- removeRestrictBackgroundWhitelist(mUid);
- // TODO: Verify expectedUnavailableError when aconfig support mainline.
- assertBackgroundNetworkAccess(false);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
-
- // Set to non-metered network
- mMeterednessConfiguration.configureNetworkMeteredness(false);
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- try {
- assertBackgroundNetworkAccess(true);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
- // Disable restrict background, should not trigger callback
- setRestrictBackground(false);
- assertBackgroundNetworkAccess(true);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- @RequiredProperties({BATTERY_SAVER_MODE})
- @Test
- public void testOnBlockedStatusChanged_powerSaver() throws Exception {
- try {
- // Enable Power Saver
- setBatterySaverMode(true);
- if (SdkLevel.isAtLeastT()) {
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkAccess(false, "java.net.UnknownHostException");
- } else {
- assertBackgroundNetworkAccess(false);
- }
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
- // Disable Power Saver
- setBatterySaverMode(false);
- assertBackgroundNetworkAccess(true);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
- assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
-
- // Set to non-metered network
- mMeterednessConfiguration.configureNetworkMeteredness(false);
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- try {
- // Enable Power Saver
- setBatterySaverMode(true);
- if (SdkLevel.isAtLeastT()) {
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkAccess(false, "java.net.UnknownHostException");
- } else {
- assertBackgroundNetworkAccess(false);
- }
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
- // Disable Power Saver
- setBatterySaverMode(false);
- assertBackgroundNetworkAccess(true);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- @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(mProcessStateTransitionLongDelayMs);
- 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(mProcessStateTransitionLongDelayMs);
- 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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
deleted file mode 100644
index 6c5f2ff..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.os.Process.SYSTEM_UID;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-
-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;
-import org.junit.Test;
-
-public class NetworkPolicyManagerTest extends AbstractRestrictBackgroundNetworkTestCase {
- private static final boolean METERED = true;
- private static final boolean NON_METERED = false;
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- registerBroadcastReceiver();
-
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(0);
-
- // Initial state
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setRestrictedNetworkingMode(false);
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setRestrictedNetworkingMode(false);
- unregisterNetworkCallback();
- stopApp();
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of non-metered network and uid not matched by any rule.
- // If mUid is not blocked by data saver mode or power saver mode, no matter the network is
- // metered or non-metered, mUid shouldn't be blocked.
- assertFalse(isUidNetworkingBlocked(mUid, METERED)); // Match NTWK_ALLOWED_DEFAULT
- assertFalse(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- }
-
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the case of uid is system uid.
- // SYSTEM_UID will never be blocked.
- assertFalse(isUidNetworkingBlocked(SYSTEM_UID, METERED)); // Match NTWK_ALLOWED_SYSTEM
- assertFalse(isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
- try {
- setRestrictBackground(true);
- setBatterySaverMode(true);
- setRestrictedNetworkingMode(true);
- assertNetworkingBlockedStatusForUid(SYSTEM_UID, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_SYSTEM
- assertFalse(
- isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
- } finally {
- setRestrictBackground(false);
- setBatterySaverMode(false);
- setRestrictedNetworkingMode(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @RequiredProperties({DATA_SAVER_MODE})
- @Test
- public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of non-metered network, uid is matched by restrict background blacklist,
- // uid is matched by restrict background whitelist, app is in the foreground with restrict
- // background enabled and the app is in the background with restrict background enabled.
- try {
- // Enable restrict background and mUid will be blocked because it's not in the
- // foreground.
- setRestrictBackground(true);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
-
- // Although restrict background is enabled and mUid is in the background, but mUid will
- // not be blocked if network is non-metered.
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-
- // Add mUid into the restrict background blacklist.
- addRestrictBackgroundBlacklist(mUid);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_DENYLIST
-
- // Although mUid is in the restrict background blacklist, but mUid won't be blocked if
- // the network is non-metered.
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- removeRestrictBackgroundBlacklist(mUid);
-
- // Add mUid into the restrict background whitelist.
- addRestrictBackgroundWhitelist(mUid);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_ALLOWLIST
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- removeRestrictBackgroundWhitelist(mUid);
-
- // Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
- launchActivity();
- assertTopState();
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
-
- // Back to background.
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
- } finally {
- setRestrictBackground(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of restricted networking mode enabled.
- try {
- // All apps should be blocked if restricted networking mode is enabled except for those
- // apps who have CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
- // This test won't test if an app who has CONNECTIVITY_USE_RESTRICTED_NETWORKS will not
- // be blocked because CONNECTIVITY_USE_RESTRICTED_NETWORKS is a signature/privileged
- // permission that CTS cannot acquire. Also it's not good for this test to use those
- // privileged apps which have CONNECTIVITY_USE_RESTRICTED_NETWORKS to test because there
- // is no guarantee that those apps won't remove this permission someday, and if it
- // happens, then this test will fail.
- setRestrictedNetworkingMode(true);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_RESTRICTED_MODE
- assertTrue(isUidNetworkingBlocked(mUid,
- NON_METERED)); // Match NTWK_BLOCKED_RESTRICTED_MODE
- } finally {
- setRestrictedNetworkingMode(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @RequiredProperties({BATTERY_SAVER_MODE})
- @Test
- public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of power saver mode enabled, uid in the power saver mode whitelist and
- // uid in the power saver mode whitelist with non-metered network.
- try {
- // mUid should be blocked if power saver mode is enabled.
- setBatterySaverMode(true);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_POWER
- assertTrue(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_BLOCKED_POWER
-
- // Add TEST_APP2_PKG into power saver mode whitelist, its uid rule is RULE_ALLOW_ALL and
- // it shouldn't be blocked.
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- } finally {
- setBatterySaverMode(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @RequiredProperties({DATA_SAVER_MODE})
- @Test
- public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
- try {
- // isUidRestrictedOnMeteredNetworks() will only return true when restrict background is
- // enabled and mUid is not in the restrict background whitelist and TEST_APP2_PKG is not
- // in the foreground. For other cases, it will return false.
- setRestrictBackground(true);
- assertIsUidRestrictedOnMeteredNetworks(mUid, true /* expectedResult */);
-
- // Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
- // return false.
- launchActivity();
- assertTopState();
- assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
- // Back to background.
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-
- // Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
- // will return false.
- addRestrictBackgroundWhitelist(mUid);
- assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
- removeRestrictBackgroundWhitelist(mUid);
- } finally {
- // Restrict background is disabled and isUidRestrictedOnMeteredNetworks() will return
- // false.
- setRestrictBackground(false);
- assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
- }
- }
-
- @Test
- public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
- assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
- try {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- 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(mProcessStateTransitionLongDelayMs);
- 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-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
deleted file mode 100644
index 0207b00..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2020 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.netpolicy.hostside;
-
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
-
-import org.junit.rules.RunRules;
-import org.junit.rules.TestRule;
-import org.junit.runners.model.FrameworkMethod;
-import org.junit.runners.model.InitializationError;
-import org.junit.runners.model.Statement;
-
-import java.util.List;
-
-/**
- * Custom runner to allow dumping logs after a test failure before the @After methods get to run.
- */
-public class NetworkPolicyTestRunner extends AndroidJUnit4ClassRunner {
- private TestRule mDumpOnFailureRule = new DumpOnFailureRule();
-
- public NetworkPolicyTestRunner(Class<?> klass) throws InitializationError {
- super(klass);
- }
-
- @Override
- public Statement methodInvoker(FrameworkMethod method, Object test) {
- return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule),
- describeChild(method));
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
deleted file mode 100644
index 26a88f2..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.location.LocationManager;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkPolicyManager;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.ActionListener;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.data.ApnSetting;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.AppStandbyUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.PollingCheck;
-import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkPolicyTestUtils {
-
- // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
- // TODO: Expose it as a @TestApi instead of copying the constant
- private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
- "carrier_metered_apn_types_strings";
-
- private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
-
- private static ConnectivityManager mCm;
- private static WifiManager mWm;
- private static CarrierConfigManager mCarrierConfigManager;
- private static NetworkPolicyManager sNpm;
-
- private static Boolean mBatterySaverSupported;
- private static Boolean mDataSaverSupported;
- private static Boolean mDozeModeSupported;
- private static Boolean mAppStandbySupported;
-
- private NetworkPolicyTestUtils() {}
-
- public static boolean isBatterySaverSupported() {
- if (mBatterySaverSupported == null) {
- mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
- }
- return mBatterySaverSupported;
- }
-
- private static boolean isWear() {
- return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
- }
-
- /**
- * As per CDD requirements, if the device doesn't support data saver mode then
- * ConnectivityManager.getRestrictBackgroundStatus() will always return
- * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
- * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
- * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
- */
- public static boolean isDataSaverSupported() {
- if (isWear()) {
- return false;
- }
- if (mDataSaverSupported == null) {
- setRestrictBackgroundInternal(false);
- assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
- try {
- setRestrictBackgroundInternal(true);
- mDataSaverSupported = !isMyRestrictBackgroundStatus(
- RESTRICT_BACKGROUND_STATUS_DISABLED);
- } finally {
- setRestrictBackgroundInternal(false);
- }
- }
- return mDataSaverSupported;
- }
-
- public static boolean isDozeModeSupported() {
- if (mDozeModeSupported == null) {
- final String result = executeShellCommand("cmd deviceidle enabled deep");
- mDozeModeSupported = result.equals("1");
- }
- return mDozeModeSupported;
- }
-
- public static boolean isAppStandbySupported() {
- if (mAppStandbySupported == null) {
- mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
- }
- return mAppStandbySupported;
- }
-
- public static boolean isLowRamDevice() {
- final ActivityManager am = (ActivityManager) getContext().getSystemService(
- Context.ACTIVITY_SERVICE);
- return am.isLowRamDevice();
- }
-
- /** Forces JobScheduler to run the job if constraints are met. */
- public static void forceRunJob(String pkg, int jobId) {
- executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId()
- + " " + pkg + " " + jobId);
- }
-
- public static boolean isLocationEnabled() {
- final LocationManager lm = (LocationManager) getContext().getSystemService(
- Context.LOCATION_SERVICE);
- return lm.isLocationEnabled();
- }
-
- public static void setLocationEnabled(boolean enabled) {
- final LocationManager lm = (LocationManager) getContext().getSystemService(
- Context.LOCATION_SERVICE);
- lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
- assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
- Log.d(TAG, "Changed location enabled state to " + enabled);
- }
-
- public static boolean isActiveNetworkMetered(boolean metered) {
- return getConnectivityManager().isActiveNetworkMetered() == metered;
- }
-
- public static boolean canChangeActiveNetworkMeteredness() {
- final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
- return networkCapabilities.hasTransport(TRANSPORT_WIFI)
- || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
- }
-
- /**
- * Updates the meteredness of the active network. Right now we can only change meteredness
- * of either Wifi or cellular network, so if the active network is not either of these, this
- * will throw an exception.
- *
- * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
- * made by this method.
- */
- public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
- if (isActiveNetworkMetered(metered)) {
- return null;
- }
- final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
- if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
- final String ssid = getWifiSsid();
- setWifiMeteredStatus(ssid, metered);
- return () -> setWifiMeteredStatus(ssid, !metered);
- } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
- final int subId = SubscriptionManager.getActiveDataSubscriptionId();
- setCellularMeteredStatus(subId, metered);
- return () -> setCellularMeteredStatus(subId, !metered);
- } else {
- // Right now, we don't have a way to change meteredness of networks other
- // than Wi-Fi or Cellular, so just throw an exception.
- throw new IllegalStateException("Can't change meteredness of current active network");
- }
- }
-
- private static String getWifiSsid() {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- final String ssid = getWifiManager().getConnectionInfo().getSSID();
- assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
- return ssid;
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-
- static NetworkCapabilities getActiveNetworkCapabilities() {
- final Network activeNetwork = getConnectivityManager().getActiveNetwork();
- assertNotNull("No active network available", activeNetwork);
- return getConnectivityManager().getNetworkCapabilities(activeNetwork);
- }
-
- private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
- currentConfig.meteredOverride = metered
- ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
- BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
- getWifiManager().save(currentConfig, createActionListener(
- blockingQueue, Integer.MAX_VALUE));
- Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
- TimeUnit.MILLISECONDS);
- if (resultCode == null) {
- fail("Timed out waiting for meteredness to change; ssid=" + ssid
- + ", metered=" + metered);
- } else if (resultCode != Integer.MAX_VALUE) {
- fail("Error overriding the meteredness; ssid=" + ssid
- + ", metered=" + metered + ", error=" + resultCode);
- }
- final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
- if (!success) {
- Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
- blockingQueue = new LinkedBlockingQueue<>();
- getWifiManager().connect(currentConfig, createActionListener(
- blockingQueue, Integer.MAX_VALUE));
- resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
- TimeUnit.MILLISECONDS);
- if (resultCode == null) {
- fail("Timed out waiting for wifi to connect; ssid=" + ssid);
- } else if (resultCode != Integer.MAX_VALUE) {
- fail("Error connecting to wifi; ssid=" + ssid
- + ", error=" + resultCode);
- }
- assertActiveNetworkMetered(metered, true /* throwOnFailure */);
- }
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-
- private static WifiConfiguration getWifiConfiguration(String ssid) {
- final List<String> ssids = new ArrayList<>();
- for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
- if (config.SSID.equals(ssid)) {
- return config;
- }
- ssids.add(config.SSID);
- }
- fail("Couldn't find the wifi config; ssid=" + ssid
- + ", all=" + Arrays.toString(ssids.toArray()));
- return null;
- }
-
- private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
- int successCode) {
- return new ActionListener() {
- @Override
- public void onSuccess() {
- blockingQueue.offer(successCode);
- }
-
- @Override
- public void onFailure(int reason) {
- blockingQueue.offer(reason);
- }
- };
- }
-
- private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
- final PersistableBundle bundle = new PersistableBundle();
- bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
- new String[] {ApnSetting.TYPE_MMS_STRING});
- ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
- (cm) -> cm.overrideConfig(subId, metered ? null : bundle));
- assertActiveNetworkMetered(metered, true /* throwOnFailure */);
- }
-
- private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
- boolean throwOnFailure) throws Exception {
- final CountDownLatch latch = new CountDownLatch(1);
- final NetworkCallback networkCallback = new NetworkCallback() {
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
- final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
- if (metered == expectedMeteredStatus) {
- latch.countDown();
- }
- }
- };
- // Registering a callback here guarantees onCapabilitiesChanged is called immediately
- // with the current setting. Therefore, if the setting has already been changed,
- // this method will return right away, and if not it will wait for the setting to change.
- getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
- try {
- if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
- final String errorMsg = "Timed out waiting for active network metered status "
- + "to change to " + expectedMeteredStatus + "; network = "
- + getConnectivityManager().getActiveNetwork();
- if (throwOnFailure) {
- fail(errorMsg);
- }
- Log.w(TAG, errorMsg);
- return false;
- }
- return true;
- } finally {
- getConnectivityManager().unregisterNetworkCallback(networkCallback);
- }
- }
-
- public static void setRestrictBackground(boolean enabled) {
- if (!isDataSaverSupported()) {
- return;
- }
- setRestrictBackgroundInternal(enabled);
- }
-
- static void setRestrictBackgroundInternal(boolean enabled) {
- executeShellCommand("cmd netpolicy set restrict-background " + enabled);
- final String output = executeShellCommand("cmd netpolicy get restrict-background");
- final String expectedSuffix = enabled ? "enabled" : "disabled";
- assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
- output.endsWith(expectedSuffix));
- }
-
- public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
- final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
- if (expectedStatus != actualStatus) {
- Log.d(TAG, "MyRestrictBackgroundStatus: "
- + "Expected: " + restrictBackgroundValueToString(expectedStatus)
- + "; Actual: " + restrictBackgroundValueToString(actualStatus));
- return false;
- }
- return true;
- }
-
- // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
- private static String unquoteSSID(String ssid) {
- // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
- // Otherwise it's guaranteed not to start with a quote.
- if (ssid.charAt(0) == '"') {
- return ssid.substring(1, ssid.length() - 1);
- } else {
- return ssid;
- }
- }
-
- public static String restrictBackgroundValueToString(int status) {
- switch (status) {
- case RESTRICT_BACKGROUND_STATUS_DISABLED:
- return "DISABLED";
- case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
- return "WHITELISTED";
- case RESTRICT_BACKGROUND_STATUS_ENABLED:
- return "ENABLED";
- default:
- return "UNKNOWN_STATUS_" + status;
- }
- }
-
- public static void clearSnoozeTimestamps() {
- executeShellCommand("dumpsys netpolicy --unsnooze");
- }
-
- public static String executeShellCommand(String command) {
- final String result = runShellCommandOrThrow(command).trim();
- Log.d(TAG, "Output of '" + command + "': '" + result + "'");
- return result;
- }
-
- public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
- final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
- assertEquals(restrictBackgroundValueToString(expectedStatus),
- restrictBackgroundValueToString(actualStatus));
- }
-
- public static ConnectivityManager getConnectivityManager() {
- if (mCm == null) {
- mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- }
- return mCm;
- }
-
- public static WifiManager getWifiManager() {
- if (mWm == null) {
- mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
- }
- return mWm;
- }
-
- public static CarrierConfigManager getCarrierConfigManager() {
- if (mCarrierConfigManager == null) {
- mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
- Context.CARRIER_CONFIG_SERVICE);
- }
- return mCarrierConfigManager;
- }
-
- public static NetworkPolicyManager getNetworkPolicyManager() {
- if (sNpm == null) {
- sNpm = getContext().getSystemService(NetworkPolicyManager.class);
- }
- return sNpm;
- }
-
- public static Context getContext() {
- return getInstrumentation().getContext();
- }
-
- public static Instrumentation getInstrumentation() {
- return InstrumentationRegistry.getInstrumentation();
- }
-
- public static UiDevice getUiDevice() {
- return UiDevice.getInstance(getInstrumentation());
- }
-
- // When power saver mode or restrict background enabled or adding any white/black list into
- // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
- // this function and using PollingCheck to try to make sure the uid has updated and reduce the
- // flaky rate.
- public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
- boolean expectedResult) {
- final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
- + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
- PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
- errMsg);
- }
-
- public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
- final String errMsg = String.format(
- "Unexpected result from isUidRestrictedOnMeteredNetworks; "
- + "uid= " + uid + ", expected=" + expectedResult);
- PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
- errMsg);
- }
-
- public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- return getNetworkPolicyManager().isUidNetworkingBlocked(uid, meteredNetwork);
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-
- public static boolean isUidRestrictedOnMeteredNetworks(int uid) {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- return getNetworkPolicyManager().isUidRestrictedOnMeteredNetworks(uid);
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
deleted file mode 100644
index a03833f..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isLowRamDevice;
-
-public enum Property {
- BATTERY_SAVER_MODE(1 << 0) {
- public boolean isSupported() { return isBatterySaverSupported(); }
- },
-
- DATA_SAVER_MODE(1 << 1) {
- public boolean isSupported() { return isDataSaverSupported(); }
- },
-
- NO_DATA_SAVER_MODE(~DATA_SAVER_MODE.getValue()) {
- public boolean isSupported() { return !isDataSaverSupported(); }
- },
-
- DOZE_MODE(1 << 2) {
- public boolean isSupported() { return isDozeModeSupported(); }
- },
-
- APP_STANDBY_MODE(1 << 3) {
- public boolean isSupported() { return isAppStandbySupported(); }
- },
-
- NOT_LOW_RAM_DEVICE(1 << 4) {
- public boolean isSupported() { return !isLowRamDevice(); }
- },
-
- METERED_NETWORK(1 << 5) {
- public boolean isSupported() {
- return isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness();
- }
- },
-
- NON_METERED_NETWORK(~METERED_NETWORK.getValue()) {
- public boolean isSupported() {
- return isActiveNetworkMetered(false) || canChangeActiveNetworkMeteredness();
- }
- };
-
- private int mValue;
-
- Property(int value) { mValue = value; }
-
- public int getValue() { return mValue; }
-
- abstract boolean isSupported();
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
deleted file mode 100644
index 799a513..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-@Retention(RUNTIME)
-@Target({METHOD, TYPE})
-@Inherited
-public @interface RequiredProperties {
- Property[] value();
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
deleted file mode 100644
index 5dea67c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-
-import org.junit.Assume;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-public class RequiredPropertiesRule extends BeforeAfterRule {
-
- private static ArraySet<Property> mRequiredProperties;
-
- @Override
- public void onBefore(Statement base, Description description) {
- mRequiredProperties = getAllRequiredProperties(description);
-
- final String testName = description.getClassName() + "#" + description.getMethodName();
- assertTestIsValid(testName, mRequiredProperties);
- Log.i(TAG, "Running test " + testName + " with required properties: "
- + propertiesToString(mRequiredProperties));
- }
-
- private ArraySet<Property> getAllRequiredProperties(Description description) {
- final ArraySet<Property> allRequiredProperties = new ArraySet<>();
- RequiredProperties requiredProperties = description.getAnnotation(RequiredProperties.class);
- if (requiredProperties != null) {
- Collections.addAll(allRequiredProperties, requiredProperties.value());
- }
-
- for (Class<?> clazz = description.getTestClass();
- clazz != null; clazz = clazz.getSuperclass()) {
- requiredProperties = clazz.getDeclaredAnnotation(RequiredProperties.class);
- if (requiredProperties == null) {
- continue;
- }
- for (Property requiredProperty : requiredProperties.value()) {
- for (Property p : Property.values()) {
- if (p.getValue() == ~requiredProperty.getValue()
- && allRequiredProperties.contains(p)) {
- continue;
- }
- }
- allRequiredProperties.add(requiredProperty);
- }
- }
- return allRequiredProperties;
- }
-
- private void assertTestIsValid(String testName, ArraySet<Property> requiredProperies) {
- if (requiredProperies == null) {
- return;
- }
- final ArrayList<Property> unsupportedProperties = new ArrayList<>();
- for (Property property : requiredProperies) {
- if (!property.isSupported()) {
- unsupportedProperties.add(property);
- }
- }
- Assume.assumeTrue("Unsupported properties: "
- + propertiesToString(unsupportedProperties), unsupportedProperties.isEmpty());
- }
-
- public static ArraySet<Property> getRequiredProperties() {
- return mRequiredProperties;
- }
-
- private static String propertiesToString(Iterable<Property> properties) {
- return "[" + TextUtils.join(",", properties) + "]";
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
deleted file mode 100644
index f183f4e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.hostside;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public final class RestrictedModeTest extends AbstractRestrictBackgroundNetworkTestCase {
- @Before
- public void setUp() throws Exception {
- super.setUp();
- setRestrictedNetworkingMode(false);
- }
-
- @After
- public void tearDown() throws Exception {
- setRestrictedNetworkingMode(false);
- super.tearDown();
- }
-
- @Test
- public void testNetworkAccess() throws Exception {
- // go to foreground state and enable restricted mode
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- setRestrictedNetworkingMode(true);
- assertTopNetworkAccess(false);
-
- // go to background state
- finishActivity();
- assertBackgroundNetworkAccess(false);
-
- // disable restricted mode and assert network access in foreground and background states
- setRestrictedNetworkingMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- assertTopNetworkAccess(true);
-
- // go to background state
- finishActivity();
- assertBackgroundNetworkAccess(true);
- }
-
- @Test
- public void testNetworkAccess_withBatterySaver() throws Exception {
- setBatterySaverMode(true);
- try {
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- setRestrictedNetworkingMode(true);
- // App would be denied network access since Restricted mode is on.
- assertBackgroundNetworkAccess(false);
- setRestrictedNetworkingMode(false);
- // Given that Restricted mode is turned off, app should be able to access network again.
- assertBackgroundNetworkAccess(true);
- } finally {
- setBatterySaverMode(false);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/Android.bp b/tests/cts/hostside-network-policy/app2/Android.bp
deleted file mode 100644
index 6ef0b06..0000000
--- a/tests/cts/hostside-network-policy/app2/Android.bp
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
- default_team: "trendy_team_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkPolicyTestsApp2",
- defaults: ["cts_support_defaults"],
- platform_apis: true,
- static_libs: [
- "androidx.annotation_annotation",
- "CtsHostsideNetworkPolicyTestsAidl",
- "modules-utils-build",
- ],
- srcs: ["src/**/*.java"],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
- certificate: ":cts-netpolicy-app",
-}
diff --git a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml b/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
deleted file mode 100644
index 668f2da..0000000
--- a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<?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.android.cts.netpolicy.hostside.app2">
-
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
- <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
- <!--
- This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
- them in a shared preferences which is then read by the test app. These broadcasts are
- handled by 2 listeners, one defined the manifest and another dynamically registered by
- a service.
-
- The manifest-defined listener also handles ordered broadcasts used to share data with the
- test app.
-
- This application also provides a service, RemoteSocketFactoryService, that the test app can
- use to open sockets to remote hosts as a different user ID.
- -->
- <application android:usesCleartextTraffic="true"
- android:testOnly="true"
- android:debuggable="true">
-
- <activity android:name=".MyActivity"
- android:exported="true"/>
- <service android:name=".MyService"
- android:exported="true"/>
- <service android:name=".MyForegroundService"
- android:foregroundServiceType="specialUse"
- android:exported="true">
- <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
- android:value="Connectivity" />
- </service>
- <receiver android:name=".MyBroadcastReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_COUNTERS"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.CHECK_NETWORK"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.SEND_NOTIFICATION"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST"/>
- </intent-filter>
- </receiver>
- <service android:name=".MyJobService"
- android:permission="android.permission.BIND_JOB_SERVICE" />
- </application>
-
- <!--
- Adding this to make sure that receiving the broadcast is not restricted by
- package visibility restrictions.
- -->
- <queries>
- <package android:name="android" />
- </queries>
-
-</manifest>
diff --git a/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png b/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
deleted file mode 100644
index 6ae570b..0000000
--- a/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
deleted file mode 100644
index 1719f9b..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
+++ /dev/null
@@ -1,234 +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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_OTHER;
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES;
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.INetworkStateObserver;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-import java.util.concurrent.TimeUnit;
-
-public final class Common {
-
- static final String TAG = "CtsNetApp2";
-
- // Constants below must match values defined on app's
- // AbstractRestrictBackgroundNetworkTestCase.java
- static final String MANIFEST_RECEIVER = "ManifestReceiver";
- static final String DYNAMIC_RECEIVER = "DynamicReceiver";
-
- static final String ACTION_RECEIVER_READY =
- "com.android.cts.netpolicy.hostside.app2.action.RECEIVER_READY";
- static final String ACTION_FINISH_ACTIVITY =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
- static final String ACTION_FINISH_JOB =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
- static final String ACTION_SHOW_TOAST =
- "com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST";
- // Copied from com.android.server.net.NetworkPolicyManagerService class
- static final String ACTION_SNOOZE_WARNING =
- "com.android.server.net.action.SNOOZE_WARNING";
-
- private static final String DEFAULT_TEST_URL =
- "https://connectivitycheck.android.com/generate_204";
-
- static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
- static final String NOTIFICATION_TYPE_DELETE = "DELETE";
- static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
- static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
- static final String NOTIFICATION_TYPE_ACTION = "ACTION";
- static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
- static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
- static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
- static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
- static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
- static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
-
- static final int TYPE_COMPONENT_ACTIVTY = 0;
- static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
- static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
- private static final int NETWORK_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
-
- static int getUid(Context context) {
- final String packageName = context.getPackageName();
- try {
- return context.getPackageManager().getPackageUid(packageName, 0);
- } catch (NameNotFoundException e) {
- throw new IllegalStateException("Could not get UID for " + packageName, e);
- }
- }
-
- private static NetworkCheckResult createNetworkCheckResult(boolean connected, String details,
- NetworkInfo networkInfo) {
- final NetworkCheckResult checkResult = new NetworkCheckResult();
- checkResult.connected = connected;
- checkResult.details = details;
- checkResult.networkInfo = networkInfo;
- return checkResult;
- }
-
- private static boolean validateComponentState(Context context, int componentType,
- INetworkStateObserver observer) throws RemoteException {
- final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
- switch (componentType) {
- case TYPE_COMPONENT_ACTIVTY: {
- final int procState = activityManager.getUidProcessState(Process.myUid());
- if (procState != ActivityManager.PROCESS_STATE_TOP) {
- observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
- createNetworkCheckResult(false, "Unexpected procstate: " + procState,
- null));
- return false;
- }
- return true;
- }
- case TYPE_COMPONENT_FOREGROUND_SERVICE: {
- final int procState = activityManager.getUidProcessState(Process.myUid());
- if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
- observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
- createNetworkCheckResult(false, "Unexpected procstate: " + procState,
- null));
- return false;
- }
- return true;
- }
- case TYPE_COMPONENT_EXPEDITED_JOB: {
- final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
- if ((capabilities
- & ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) == 0) {
- observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_CAPABILITIES,
- createNetworkCheckResult(false,
- "Unexpected capabilities: " + capabilities, null));
- return false;
- }
- return true;
- }
- default: {
- observer.onNetworkStateChecked(RESULT_ERROR_OTHER,
- createNetworkCheckResult(false, "Unknown component type: " + componentType,
- null));
- return false;
- }
- }
- }
-
- static void notifyNetworkStateObserver(Context context, Intent intent, int componentType) {
- if (intent == null) {
- return;
- }
- final Bundle extras = intent.getExtras();
- notifyNetworkStateObserver(context, extras, componentType);
- }
-
- static void notifyNetworkStateObserver(Context context, Bundle extras, int componentType) {
- if (extras == null) {
- return;
- }
- final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
- extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
- if (observer != null) {
- final String customUrl = extras.getString(KEY_CUSTOM_URL);
- try {
- final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS);
- if (!skipValidation && !validateComponentState(context, componentType, observer)) {
- return;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error occurred while informing the validation result: " + e);
- }
- AsyncTask.execute(() -> {
- try {
- observer.onNetworkStateChecked(
- INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
- checkNetworkStatus(context, customUrl));
- } catch (RemoteException e) {
- Log.e(TAG, "Error occurred while notifying the observer: " + e);
- }
- });
- }
- }
-
- /**
- * Checks whether the network is available by attempting a connection to the given address
- * and returns a {@link NetworkCheckResult} object containing all the relevant details for
- * debugging. Uses a default address if the given address is {@code null}.
- *
- * <p>
- * The returned object has the following fields:
- *
- * <ul>
- * <li>{@code connected}: whether or not the connection was successful.
- * <li>{@code networkInfo}: the {@link NetworkInfo} describing the current active network as
- * visible to this app.
- * <li>{@code details}: A human readable string giving useful information about the success or
- * failure.
- * </ul>
- */
- static NetworkCheckResult checkNetworkStatus(Context context, String customUrl) {
- final String address = (customUrl == null) ? DEFAULT_TEST_URL : customUrl;
-
- // The current Android DNS resolver returns an UnknownHostException whenever network access
- // is blocked. This can get cached in the current process-local InetAddress cache. Clearing
- // the cache before attempting a connection ensures we never report a failure due to a
- // negative cache entry.
- InetAddress.clearDnsCache();
-
- final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
-
- final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- Log.d(TAG, "Running checkNetworkStatus() on thread "
- + Thread.currentThread().getName() + " for UID " + getUid(context)
- + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
- boolean checkStatus = false;
- String checkDetails = "N/A";
- try {
- final URL url = new URL(address);
- final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setReadTimeout(NETWORK_TIMEOUT_MS);
- conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
- conn.setRequestMethod("GET");
- conn.connect();
- final int response = conn.getResponseCode();
- checkStatus = true;
- checkDetails = "HTTP response for " + address + ": " + response;
- } catch (Exception e) {
- checkStatus = false;
- checkDetails = "Exception getting " + address + ": " + e;
- }
- final NetworkCheckResult result = createNetworkCheckResult(checkStatus, checkDetails,
- networkInfo);
- Log.d(TAG, "Offering: " + result);
- return result;
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
deleted file mode 100644
index d274c50..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
+++ /dev/null
@@ -1,102 +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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.RemoteCallback;
-import android.util.Log;
-import android.view.WindowManager;
-
-import androidx.annotation.GuardedBy;
-
-/**
- * Activity used to bring process to foreground.
- */
-public class MyActivity extends Activity {
-
- @GuardedBy("this")
- private BroadcastReceiver finishCommandReceiver = null;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.d(TAG, "MyActivity.onCreate()");
-
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- }
-
- @Override
- public void finish() {
- synchronized (this) {
- if (finishCommandReceiver != null) {
- unregisterReceiver(finishCommandReceiver);
- finishCommandReceiver = null;
- }
- }
- super.finish();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- Log.d(TAG, "MyActivity.onStart()");
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- Log.d(TAG, "MyActivity.onNewIntent()");
- setIntent(intent);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- Log.d(TAG, "MyActivity.onResume(): " + getIntent());
- Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
- synchronized (this) {
- finishCommandReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "Finishing MyActivity");
- MyActivity.this.finish();
- }
- };
- registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
- Context.RECEIVER_EXPORTED);
- }
- final RemoteCallback callback = getIntent().getParcelableExtra(
- Intent.EXTRA_REMOTE_CALLBACK);
- if (callback != null) {
- callback.sendResult(null);
- }
- }
-
- @Override
- protected void onDestroy() {
- Log.d(TAG, "MyActivity.onDestroy()");
- super.onDestroy();
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
deleted file mode 100644
index 27aec8c..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
+++ /dev/null
@@ -1,193 +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.cts.netpolicy.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SHOW_TOAST;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.netpolicy.hostside.app2.Common.MANIFEST_RECEIVER;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.RemoteInput;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Toast;
-
-/**
- * Receiver used to:
- * <ol>
- * <li>Count number of {@code RESTRICT_BACKGROUND_CHANGED} broadcasts received.
- * <li>Show a toast.
- * </ol>
- */
-public class MyBroadcastReceiver extends BroadcastReceiver {
-
- private final String mName;
-
- public MyBroadcastReceiver() {
- this(MANIFEST_RECEIVER);
- }
-
- MyBroadcastReceiver(String name) {
- Log.d(TAG, "Constructing MyBroadcastReceiver named " + name);
- mName = name;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "onReceive() for " + mName + ": " + intent);
- final String action = intent.getAction();
- switch (action) {
- case ACTION_SNOOZE_WARNING:
- increaseCounter(context, action);
- break;
- case ACTION_RESTRICT_BACKGROUND_CHANGED:
- increaseCounter(context, action);
- break;
- case ACTION_RECEIVER_READY:
- final String message = mName + " is ready to rumble";
- Log.d(TAG, message);
- setResultData(message);
- break;
- case ACTION_SHOW_TOAST:
- showToast(context);
- break;
- default:
- Log.e(TAG, "received unexpected action: " + action);
- }
- }
-
- @Override
- public String toString() {
- return "[MyBroadcastReceiver: mName=" + mName + "]";
- }
-
- private void increaseCounter(Context context, String action) {
- final SharedPreferences prefs = context.getApplicationContext()
- .getSharedPreferences(mName, Context.MODE_PRIVATE);
- final int value = prefs.getInt(action, 0) + 1;
- Log.d(TAG, "increaseCounter('" + action + "'): setting '" + mName + "' to " + value);
- prefs.edit().putInt(action, value).apply();
- }
-
- static int getCounter(Context context, String action, String receiverName) {
- final SharedPreferences prefs = context.getSharedPreferences(receiverName,
- Context.MODE_PRIVATE);
- final int value = prefs.getInt(action, 0);
- Log.d(TAG, "getCounter('" + action + "', '" + receiverName + "'): " + value);
- return value;
- }
-
- static String getRestrictBackgroundStatus(Context context) {
- final ConnectivityManager cm = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- final int apiStatus = cm.getRestrictBackgroundStatus();
- Log.d(TAG, "getRestrictBackgroundStatus: returning " + apiStatus);
- return String.valueOf(apiStatus);
- }
-
- /**
- * Sends a system notification containing actions with pending intents to launch the app's
- * main activitiy or service.
- */
- static void sendNotification(Context context, String channelId, int notificationId,
- String notificationType ) {
- Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType);
- final Intent serviceIntent = new Intent(context, MyService.class);
- final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent,
- PendingIntent.FLAG_MUTABLE);
- final Bundle bundle = new Bundle();
- bundle.putCharSequence("parcelable", "I am not");
-
- final Notification.Builder builder = new Notification.Builder(context, channelId)
- .setSmallIcon(R.drawable.ic_notification);
-
- Action action = null;
- switch (notificationType) {
- case NOTIFICATION_TYPE_CONTENT:
- builder
- .setContentTitle("Light, Cameras...")
- .setContentIntent(pendingIntent);
- break;
- case NOTIFICATION_TYPE_DELETE:
- builder.setDeleteIntent(pendingIntent);
- break;
- case NOTIFICATION_TYPE_FULL_SCREEN:
- builder.setFullScreenIntent(pendingIntent, true);
- break;
- case NOTIFICATION_TYPE_BUNDLE:
- bundle.putParcelable("Magnum P.I. (Pending Intent)", pendingIntent);
- builder.setExtras(bundle);
- break;
- case NOTIFICATION_TYPE_ACTION:
- action = new Action.Builder(
- R.drawable.ic_notification, "ACTION", pendingIntent)
- .build();
- builder.addAction(action);
- break;
- case NOTIFICATION_TYPE_ACTION_BUNDLE:
- bundle.putParcelable("Magnum A.P.I. (Action Pending Intent)", pendingIntent);
- action = new Action.Builder(
- R.drawable.ic_notification, "ACTION WITH BUNDLE", null)
- .addExtras(bundle)
- .build();
- builder.addAction(action);
- break;
- case NOTIFICATION_TYPE_ACTION_REMOTE_INPUT:
- bundle.putParcelable("Magnum R.I. (Remote Input)", null);
- final RemoteInput remoteInput = new RemoteInput.Builder("RI")
- .addExtras(bundle)
- .build();
- action = new Action.Builder(
- R.drawable.ic_notification, "ACTION WITH REMOTE INPUT", pendingIntent)
- .addRemoteInput(remoteInput)
- .build();
- builder.addAction(action);
- break;
- default:
- Log.e(TAG, "Unknown notification type: " + notificationType);
- return;
- }
-
- final Notification notification = builder.build();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
- .notify(notificationId, notification);
- }
-
- private void showToast(Context context) {
- Toast.makeText(context, "Toast from CTS test", Toast.LENGTH_SHORT).show();
- setResultData("Shown");
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
deleted file mode 100644
index 54cee3c..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
+++ /dev/null
@@ -1,73 +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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TEST_PKG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_FOREGROUND_SERVICE;
-
-import android.R;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.INetworkStateObserver;
-
-/**
- * Service used to change app state to FOREGROUND_SERVICE.
- */
-public class MyForegroundService extends Service {
- private static final String NOTIFICATION_CHANNEL_ID = "cts/MyForegroundService";
- private static final int FLAG_START_FOREGROUND = 1;
- private static final int FLAG_STOP_FOREGROUND = 2;
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- Log.v(TAG, "MyForegroundService.onStartCommand(): " + intent);
- NotificationManager notificationManager = getSystemService(NotificationManager.class);
- notificationManager.createNotificationChannel(new NotificationChannel(
- NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
- NotificationManager.IMPORTANCE_DEFAULT));
- switch (intent.getFlags()) {
- case FLAG_START_FOREGROUND:
- Log.d(TAG, "Starting foreground");
- startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine
- .build());
- Common.notifyNetworkStateObserver(this, intent, TYPE_COMPONENT_FOREGROUND_SERVICE);
- break;
- case FLAG_STOP_FOREGROUND:
- Log.d(TAG, "Stopping foreground");
- stopForeground(true);
- break;
- default:
- Log.wtf(TAG, "Invalid flag on intent " + intent);
- }
- return START_STICKY;
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
deleted file mode 100644
index eba55ed..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_JOB;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_EXPEDITED_JOB;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-public class MyJobService extends JobService {
-
- private BroadcastReceiver mFinishCommandReceiver = null;
-
- @Override
- public void onCreate() {
- super.onCreate();
- Log.v(TAG, "MyJobService.onCreate()");
- }
-
- @Override
- public boolean onStartJob(JobParameters params) {
- Log.v(TAG, "MyJobService.onStartJob()");
- Common.notifyNetworkStateObserver(this, params.getTransientExtras(),
- TYPE_COMPONENT_EXPEDITED_JOB);
- mFinishCommandReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.v(TAG, "Finishing MyJobService");
- try {
- jobFinished(params, /*wantsReschedule=*/ false);
- } finally {
- if (mFinishCommandReceiver != null) {
- unregisterReceiver(mFinishCommandReceiver);
- mFinishCommandReceiver = null;
- }
- }
- }
- };
- registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB),
- Context.RECEIVER_EXPORTED);
- return true;
- }
-
- @Override
- public boolean onStopJob(JobParameters params) {
- // If this job is stopped before it had a chance to send network status via
- // INetworkStateObserver, the test will fail. It could happen either due to test timing out
- // or this app moving to a lower proc_state and losing network access.
- Log.v(TAG, "MyJobService.onStopJob()");
- if (mFinishCommandReceiver != null) {
- unregisterReceiver(mFinishCommandReceiver);
- mFinishCommandReceiver = null;
- }
- return false;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Log.v(TAG, "MyJobService.onDestroy()");
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
deleted file mode 100644
index 71bcead..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
+++ /dev/null
@@ -1,200 +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.cts.netpolicy.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.netpolicy.hostside.app2.Common.DYNAMIC_RECEIVER;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.IMyService;
-import com.android.cts.netpolicy.hostside.INetworkCallback;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-import com.android.modules.utils.build.SdkLevel;
-
-/**
- * Service used to dynamically register a broadcast receiver.
- */
-public class MyService extends Service {
- private static final String NOTIFICATION_CHANNEL_ID = "MyService";
-
- ConnectivityManager mCm;
-
- private MyBroadcastReceiver mReceiver;
- private ConnectivityManager.NetworkCallback mNetworkCallback;
-
- // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
-
- private IMyService.Stub mBinder = new IMyService.Stub() {
- @Override
- public void registerBroadcastReceiver() {
- if (mReceiver != null) {
- Log.d(TAG, "receiver already registered: " + mReceiver);
- return;
- }
- final Context context = getApplicationContext();
- final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
- mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER);
- context.registerReceiver(mReceiver,
- new IntentFilter(ACTION_RECEIVER_READY), flags);
- context.registerReceiver(mReceiver,
- new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED), flags);
- context.registerReceiver(mReceiver,
- new IntentFilter(ACTION_SNOOZE_WARNING), flags);
- Log.d(TAG, "receiver registered");
- }
-
- @Override
- public int getCounters(String receiverName, String action) {
- return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName);
- }
-
- @Override
- public NetworkCheckResult checkNetworkStatus(String customUrl) {
- return Common.checkNetworkStatus(getApplicationContext(), customUrl);
- }
-
- @Override
- public String getRestrictBackgroundStatus() {
- return MyBroadcastReceiver.getRestrictBackgroundStatus(getApplicationContext());
- }
-
- @Override
- public void sendNotification(int notificationId, String notificationType) {
- MyBroadcastReceiver.sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
- notificationId, notificationType);
- }
-
- @Override
- public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb) {
- if (mNetworkCallback != null) {
- Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
- unregisterNetworkCallback();
- }
- Log.d(TAG, "registering network callback for " + request);
-
- mNetworkCallback = new ConnectivityManager.NetworkCallback() {
- @Override
- public void onBlockedStatusChanged(Network network, boolean blocked) {
- try {
- cb.onBlockedStatusChanged(network, blocked);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onBlockedStatusChanged: " + e);
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void onAvailable(Network network) {
- try {
- cb.onAvailable(network);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onAvailable: " + e);
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void onLost(Network network) {
- try {
- cb.onLost(network);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onLost: " + e);
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
- try {
- cb.onCapabilitiesChanged(network, cap);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e);
- unregisterNetworkCallback();
- }
- }
- };
- mCm.registerNetworkCallback(request, mNetworkCallback);
- try {
- cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
- } catch (RemoteException e) {
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void unregisterNetworkCallback() {
- Log.d(TAG, "unregistering network callback");
- if (mNetworkCallback != null) {
- mCm.unregisterNetworkCallback(mNetworkCallback);
- mNetworkCallback = null;
- }
- }
-
- @Override
- public int scheduleJob(JobInfo jobInfo) {
- final JobScheduler jobScheduler = getApplicationContext()
- .getSystemService(JobScheduler.class);
- return jobScheduler.schedule(jobInfo);
- }
- };
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-
- @Override
- public void onCreate() {
- final Context context = getApplicationContext();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
- .createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID,
- NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
- mCm = (ConnectivityManager) getApplicationContext()
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- }
-
- @Override
- public void onDestroy() {
- final Context context = getApplicationContext();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
- .deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
- if (mReceiver != null) {
- Log.d(TAG, "onDestroy(): unregistering " + mReceiver);
- getApplicationContext().unregisterReceiver(mReceiver);
- }
-
- super.onDestroy();
- }
-}
diff --git a/tests/cts/hostside-network-policy/certs/Android.bp b/tests/cts/hostside-network-policy/certs/Android.bp
deleted file mode 100644
index bfbc341..0000000
--- a/tests/cts/hostside-network-policy/certs/Android.bp
+++ /dev/null
@@ -1,9 +0,0 @@
-package {
- default_team: "trendy_team_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app_certificate {
- name: "cts-netpolicy-app",
- certificate: "cts-net-app",
-}
diff --git a/tests/cts/hostside-network-policy/certs/README b/tests/cts/hostside-network-policy/certs/README
deleted file mode 100644
index b660a82..0000000
--- a/tests/cts/hostside-network-policy/certs/README
+++ /dev/null
@@ -1,2 +0,0 @@
-# Generated with:
-development/tools/make_key cts-net-app '/CN=cts-net-app'
diff --git a/tests/cts/hostside-network-policy/certs/cts-net-app.pk8 b/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
deleted file mode 100644
index 1703e4e..0000000
--- a/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem b/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
deleted file mode 100644
index a15ff48..0000000
--- a/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDAjCCAeqgAwIBAgIJAMhWwIIqr1r6MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
-BAMMC2N0cy1uZXQtYXBwMB4XDTE4MDYyMDAyMjAwN1oXDTQ1MTEwNTAyMjAwN1ow
-FjEUMBIGA1UEAwwLY3RzLW5ldC1hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQDefOayWQss1E+FQIONK6IhlXhe0BEyHshIrnPOOmuCPa/Svfbnmziy
-hr1KTjaQ3ET/mGShwlt6AUti7nKx9aB71IJp5mSBuwW62A8jvN3yNOo45YV8+n1o
-TrEoMWMf7hQmoOSqaSJ+VFuVms/kPSEh99okDgHCej6rsEkEcDoh6pJajQyUYDwR
-SNAF8SrqCDhqFbZW/LWedvuikCUlNtzuv7/GrcLcsiWEfHv7UOBKpMjLo9BhD1XF
-IefnxImcBQrQGMnE9TLixBiEeX5yauLgbZuxBqD/zsI2TH1FjxTeuJan83kLbqqH
-FgyvPaUjwckAdQPyom7ZUYFnBc0LQ9xzAgMBAAGjUzBRMB0GA1UdDgQWBBRZrBEw
-tAB2WNXj8dQ7ZOuJ34kY5DAfBgNVHSMEGDAWgBRZrBEwtAB2WNXj8dQ7ZOuJ34kY
-5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeI9AnLW6l/39y
-z96w/ldxZVFPzBRiFIsJsPHVyXlD5vUHZv/ju2jFn8TZSZR5TK0bzCEoVLp34Sho
-bbS0magP82yIvCRibyoyD+TDNnZkNJwjYnikE+/oyshTSQtpkn/rDA+0Y09BUC1E
-N2I6bV9pTXLFg7oah2FmqPRPzhgeYUKENgOQkrrjUCn6y0i/k374n7aftzdniSIz
-2kCRVEeN9gws6CnoMPx0vr32v/JVuPV6zfdJYadgj/eFRyTNE4msd9kE82Wc46eU
-YiI+LuXZ3ZMUNWGY7MK2pOUUS52JsBQ3K235dA5WaU4x8OBlY/WkNYX/eLbNs5jj
-FzLmhZZ1
------END CERTIFICATE-----
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp b/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
deleted file mode 100644
index cdede36..0000000
--- a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-filegroup {
- name: "ArgumentConstants",
- srcs: ["src/**/*.java"],
-}
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java b/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
deleted file mode 100644
index 0fe98e9..0000000
--- a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy.arguments;
-
-public interface InstrumentationArguments {
- String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
- String ARG_CONNECTION_CHECK_CUSTOM_URL = "connection_check_custom_url";
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
deleted file mode 100644
index 422231d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
+++ /dev/null
@@ -1,74 +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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
-import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-
-import org.junit.Test;
-
-import java.util.Map;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideConnOnActivityStartTest extends HostsideNetworkPolicyTestCase {
- private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
-
- @BeforeClassWithInfo
- public static void setUpOnce(TestInformation testInfo) throws Exception {
- uninstallPackage(testInfo, TEST_APP2_PKG, false);
- installPackage(testInfo, TEST_APP2_APK);
- }
-
- @AfterClassWithInfo
- public static void tearDownOnce(TestInformation testInfo) throws DeviceNotAvailableException {
- uninstallPackage(testInfo, TEST_APP2_PKG, true);
- }
-
- @Test
- public void testStartActivity_batterySaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
- }
-
- @Test
- public void testStartActivity_dataSaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
- }
-
- @FlakyTest(bugId = 231440256)
- @Test
- public void testStartActivity_doze() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
- }
-
- @Test
- public void testStartActivity_appStandby() throws Exception {
- runDeviceTestsWithCustomOptions(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 {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_default",
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
deleted file mode 100644
index 62952bb..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-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.
-@FlakyTest(bugId = 288324467)
-public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkPolicyTestCase {
- 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 {
- runDeviceTestsWithCustomOptions(TEST_PKG, METERED_TEST_CLASS, methodName,
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-
- private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
- runDeviceTestsWithCustomOptions(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-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
deleted file mode 100644
index 2c2b118..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2019 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.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideNetworkCallbackTests extends HostsideNetworkPolicyTestCase {
-
- @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);
- }
-
- @Test
- public void testOnBlockedStatusChanged_dataSaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
- }
-
- @Test
- public void testOnBlockedStatusChanged_powerSaver() throws Exception {
- runDeviceTestsWithCustomOptions(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 {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
- "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-}
-
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
deleted file mode 100644
index 8ffe360..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy;
-
-import static com.android.cts.netpolicy.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 HostsideNetworkPolicyTestCase {
- @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);
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withUidNotBlocked");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withDataSaverMode");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withPowerSaverMode");
- }
-
- @Test
- public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
- runDeviceTestsWithCustomOptions(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 {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_whenInBackground",
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
deleted file mode 100644
index 6de6b17..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.targetprep.BuildError;
-import com.android.tradefed.targetprep.TargetSetupError;
-import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-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 HostsideNetworkPolicyTestCase extends BaseHostJUnit4Test {
- protected static final boolean DEBUG = false;
- protected static final String TAG = "HostsideNetworkPolicyTests";
- protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
- protected static final String TEST_APK = "CtsHostsideNetworkPolicyTestsApp.apk";
- protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
- protected static final String TEST_APP2_APK = "CtsHostsideNetworkPolicyTestsApp2.apk";
-
- @Option(name = "custom-url", importance = Option.Importance.IF_UNSET,
- description = "A custom url to use for testing network connections")
- protected String mCustomUrl;
-
- @BeforeClassWithInfo
- public static void setUpOnceBase(TestInformation testInfo) throws Exception {
- uninstallPackage(testInfo, TEST_PKG, false);
- installPackage(testInfo, TEST_APK);
- }
-
- @AfterClassWithInfo
- public static void tearDownOnceBase(TestInformation testInfo)
- throws DeviceNotAvailableException {
- uninstallPackage(testInfo, TEST_PKG, true);
- }
-
- // Custom static method to install the specified package, this is used to bypass auto-cleanup
- // per test in BaseHostJUnit4.
- protected static void installPackage(TestInformation testInfo, String apk)
- throws DeviceNotAvailableException, TargetSetupError {
- assertNotNull(testInfo);
- final int userId = testInfo.getDevice().getCurrentUser();
- final SuiteApkInstaller installer = new SuiteApkInstaller();
- // Force the apk clean up
- installer.setCleanApk(true);
- installer.addTestFileName(apk);
- installer.setUserId(userId);
- installer.setShouldGrantPermission(true);
- installer.addInstallArg("-t");
- try {
- installer.setUp(testInfo);
- } catch (BuildError e) {
- throw new TargetSetupError(
- e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor(), e.getErrorId());
- }
- }
-
- protected void installPackage(String apk) throws DeviceNotAvailableException, TargetSetupError {
- installPackage(getTestInformation(), apk);
- }
-
- protected static void uninstallPackage(TestInformation testInfo, String packageName,
- boolean shouldSucceed)
- throws DeviceNotAvailableException {
- assertNotNull(testInfo);
- final String result = testInfo.getDevice().uninstallPackage(packageName);
- if (shouldSucceed) {
- assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
- }
- }
-
- protected void uninstallPackage(String packageName,
- boolean shouldSucceed)
- throws DeviceNotAvailableException {
- uninstallPackage(getTestInformation(), packageName, shouldSucceed);
- }
-
- protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException {
- final String command = "cmd package list packages " + packageName;
- final int max_tries = 5;
- for (int i = 1; i <= max_tries; i++) {
- final String result = runCommand(command);
- if (result.trim().isEmpty()) {
- return;
- }
- // 'list packages' filters by substring, so we need to iterate with the results
- // and check one by one, otherwise 'com.android.cts.netpolicy.hostside' could return
- // 'com.android.cts.netpolicy.hostside.app2'
- boolean found = false;
- for (String line : result.split("[\\r\\n]+")) {
- if (line.endsWith(packageName)) {
- found = true;
- break;
- }
- }
- if (!found) {
- return;
- }
- Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
- + "); sleeping 1s before polling again");
- RunUtil.getDefault().sleep(1000);
- }
- fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
- }
-
- protected int getUid(String packageName) throws DeviceNotAvailableException {
- final int currentUser = getDevice().getCurrentUser();
- final String uidLines = runCommand(
- "cmd package list packages -U --user " + currentUser + " " + packageName);
- for (String uidLine : uidLines.split("\n")) {
- if (uidLine.startsWith("package:" + packageName + " uid:")) {
- final String[] uidLineParts = uidLine.split(":");
- // 3rd entry is package uid
- return Integer.parseInt(uidLineParts[2].trim());
- }
- }
- throw new IllegalStateException("Failed to find the test app on the device; pkg="
- + packageName + ", u=" + currentUser);
- }
-
- protected boolean runDeviceTestsWithCustomOptions(String packageName, String className)
- throws DeviceNotAvailableException {
- return runDeviceTestsWithCustomOptions(packageName, className, null);
- }
-
- protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
- String methodName) throws DeviceNotAvailableException {
- return runDeviceTestsWithCustomOptions(packageName, className, methodName, null);
- }
-
- protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
- String methodName, Map<String, String> testArgs) throws DeviceNotAvailableException {
- final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
- .setTestClassName(className)
- .setTestMethodName(methodName);
-
- // Currently there is only one custom option that the test exposes.
- if (mCustomUrl != null) {
- deviceTestRunOptions.addInstrumentationArg(ARG_CONNECTION_CHECK_CUSTOM_URL, mCustomUrl);
- }
- // Pass over any test specific arguments.
- if (testArgs != null) {
- for (Map.Entry<String, String> arg : testArgs.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);
- if (DEBUG) Log.v(TAG, "Output: " + output.trim());
- return output;
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
deleted file mode 100644
index 0261c7d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
+++ /dev/null
@@ -1,466 +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.cts.netpolicy;
-
-import static org.junit.Assert.fail;
-
-import android.platform.test.annotations.FlakyTest;
-import android.platform.test.annotations.SecurityTest;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkPolicyTestCase {
-
- @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);
- }
-
- @SecurityTest
- @Test
- public void testDataWarningReceiver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
- "testSnoozeWarningNotReceived");
- }
-
- /**************************
- * Data Saver Mode tests. *
- **************************/
-
- @Test
- public void testDataSaverMode_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_disabled");
- }
-
- @Test
- public void testDataSaverMode_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_whitelisted");
- }
-
- @Test
- public void testDataSaverMode_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_enabled");
- }
-
- @Test
- public void testDataSaverMode_blacklisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_blacklisted");
- }
-
- @Test
- public void testDataSaverMode_reinstall() throws Exception {
- final int oldUid = getUid(TEST_APP2_PKG);
-
- // Make sure whitelist is revoked when package is removed
- addRestrictBackgroundWhitelist(oldUid);
-
- uninstallPackage(TEST_APP2_PKG, true);
- assertPackageUninstalled(TEST_APP2_PKG);
- assertRestrictBackgroundWhitelist(oldUid, false);
-
- installPackage(TEST_APP2_APK);
- final int newUid = getUid(TEST_APP2_PKG);
- assertRestrictBackgroundWhitelist(oldUid, false);
- assertRestrictBackgroundWhitelist(newUid, false);
- }
-
- @Test
- public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
- }
-
- @Test
- public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testBroadcastNotSentOnUnsupportedDevices");
- }
-
- /*****************************
- * Battery Saver Mode tests. *
- *****************************/
-
- @Test
- public void testBatterySaverModeMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testBatterySaverModeMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testBatterySaverModeMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testBatterySaverMode_reinstall() throws Exception {
- if (!isDozeModeEnabled()) {
- Log.w(TAG, "testBatterySaverMode_reinstall() skipped because device does not support "
- + "Doze Mode");
- return;
- }
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
-
- uninstallPackage(TEST_APP2_PKG, true);
- assertPackageUninstalled(TEST_APP2_PKG);
- assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
-
- installPackage(TEST_APP2_APK);
- assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
- }
-
- @Test
- public void testBatterySaverModeNonMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testBatterySaverModeNonMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- /*******************
- * App idle tests. *
- *******************/
-
- @Test
- public void testAppIdleMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testAppIdleMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testAppIdleMetered_tempWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_tempWhitelisted");
- }
-
- @Test
- public void testAppIdleMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testAppIdleMetered_idleWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testAppIdleNetworkAccess_idleWhitelisted");
- }
-
- // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
- // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
- // public void testAppIdle_reinstall() throws Exception {
- // }
-
- @Test
- public void testAppIdleNonMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
-
- @Test
- public void testAppIdleNonMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_tempWhitelisted");
- }
-
- @Test
- public void testAppIdleNonMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testAppIdleNetworkAccess_idleWhitelisted");
- }
-
- @Test
- public void testAppIdleNonMetered_whenCharging() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testAppIdleNetworkAccess_whenCharging");
- }
-
- @Test
- public void testAppIdleMetered_whenCharging() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testAppIdleNetworkAccess_whenCharging");
- }
-
- @Test
- public void testAppIdle_toast() throws Exception {
- // Check that showing a toast doesn't bring an app out of standby
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testAppIdle_toast");
- }
-
- /********************
- * Doze Mode tests. *
- ********************/
-
- @Test
- public void testDozeModeMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testDozeModeMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testDozeModeMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
- }
-
- // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
- // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
- // public void testDozeMode_reinstall() throws Exception {
- // }
-
- @Test
- public void testDozeModeNonMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testDozeModeNonMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testDozeModeNonMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
- throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
- }
-
- /**********************
- * Mixed modes tests. *
- **********************/
-
- @Test
- public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDataAndBatterySaverModes_meteredNetwork");
- }
-
- @Test
- public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDataAndBatterySaverModes_nonMeteredNetwork");
- }
-
- @Test
- public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDozeAndBatterySaverMode_powerSaveWhitelists");
- }
-
- @Test
- public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDozeAndAppIdle_powerSaveWhitelists");
- }
-
- @Test
- public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndDoze_tempPowerSaveWhitelists");
- }
-
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
- }
-
- @Test
- public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDozeAndAppIdle_appIdleWhitelist");
- }
-
- @Test
- public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
- }
-
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
- }
-
- /**************************
- * Restricted mode tests. *
- **************************/
-
- @Test
- public void testNetworkAccess_restrictedMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
- "testNetworkAccess");
- }
-
- @Test
- public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
- "testNetworkAccess_withBatterySaver");
- }
-
- /************************
- * Expedited job tests. *
- ************************/
-
- @Test
- public void testMeteredNetworkAccess_expeditedJob() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
- }
-
- @Test
- public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
- }
-
- /*******************
- * Helper methods. *
- *******************/
-
- private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
- final int max_tries = 5;
- boolean actual = false;
- for (int i = 1; i <= max_tries; i++) {
- final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
- actual = output.contains(Integer.toString(uid));
- if (expected == actual) {
- return;
- }
- Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
- + expected + ", got " + actual + "); sleeping 1s before polling again");
- RunUtil.getDefault().sleep(1000);
- }
- fail("whitelist check for uid " + uid + " failed: expected "
- + expected + ", got " + actual);
- }
-
- private void assertPowerSaveModeWhitelist(String packageName, boolean expected)
- throws Exception {
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- assertDelayedCommand("dumpsys deviceidle whitelist =" + packageName,
- Boolean.toString(expected));
- }
-
- /**
- * Asserts the result of a command, wait and re-running it a couple times if necessary.
- */
- private void assertDelayedCommand(String command, String expectedResult)
- throws InterruptedException, DeviceNotAvailableException {
- final int maxTries = 5;
- for (int i = 1; i <= maxTries; i++) {
- final String result = runCommand(command).trim();
- if (result.equals(expectedResult)) return;
- Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
- + expectedResult + "' on attempt #; sleeping 1s before polling again");
- RunUtil.getDefault().sleep(1000);
- }
- fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries
- + " attempts");
- }
-
- protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
- runCommand("cmd netpolicy add restrict-background-whitelist " + uid);
- assertRestrictBackgroundWhitelist(uid, true);
- }
-
- private void addPowerSaveModeWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- runCommand("dumpsys deviceidle whitelist +" + packageName);
- assertPowerSaveModeWhitelist(packageName, true);
- }
-
- protected boolean isDozeModeEnabled() throws Exception {
- final String result = runCommand("cmd deviceidle enabled deep").trim();
- return result.equals("1");
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
deleted file mode 100644
index cbf2f4d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2020 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.netpolicy;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.log.LogUtil;
-import com.android.tradefed.targetprep.ITargetPreparer;
-
-public class NetworkPolicyTestsPreparer implements ITargetPreparer {
- private ITestDevice mDevice;
- private boolean mOriginalAirplaneModeEnabled;
- private String mOriginalAppStandbyEnabled;
- private String mOriginalBatteryStatsConstants;
- private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms";
- private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0;
-
- @Override
- public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
- mDevice = testInformation.getDevice();
- mOriginalAppStandbyEnabled = getAppStandbyEnabled();
- setAppStandbyEnabled("1");
- LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
-
- mOriginalBatteryStatsConstants = getBatteryStatsConstants();
- setBatteryStatsConstants(
- KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS);
- LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants);
-
- mOriginalAirplaneModeEnabled = getAirplaneModeEnabled();
- // Turn off airplane mode in case another test left the device in that state.
- setAirplaneModeEnabled(false);
- LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled);
- }
-
- @Override
- public void tearDown(TestInformation testInformation, Throwable e)
- throws DeviceNotAvailableException {
- setAirplaneModeEnabled(mOriginalAirplaneModeEnabled);
- setAppStandbyEnabled(mOriginalAppStandbyEnabled);
- setBatteryStatsConstants(mOriginalBatteryStatsConstants);
- }
-
- private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException {
- executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable"));
- }
-
- private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException {
- return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim());
- }
-
- private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
- if ("null".equals(appStandbyEnabled)) {
- executeCmd("settings delete global app_standby_enabled");
- } else {
- executeCmd("settings put global app_standby_enabled " + appStandbyEnabled);
- }
- }
-
- private String getAppStandbyEnabled() throws DeviceNotAvailableException {
- return executeCmd("settings get global app_standby_enabled").trim();
- }
-
- private void setBatteryStatsConstants(String batteryStatsConstants)
- throws DeviceNotAvailableException {
- executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\"");
- }
-
- private String getBatteryStatsConstants() throws DeviceNotAvailableException {
- return executeCmd("settings get global battery_stats_constants");
- }
-
- private String executeCmd(String cmd) throws DeviceNotAvailableException {
- final String output = mDevice.executeShellCommand(cmd).trim();
- LogUtil.CLog.d("Output for '%s': %s", cmd, output);
- return output;
- }
-}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 14d5d54..97be91a 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -12,17 +12,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-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"],
}
+java_defaults {
+ name: "CtsHostsideNetworkTestsAllAppDefaults",
+ platform_apis: true,
+ min_sdk_version: "30",
+ // Set target SDK to 10000 so that all the test helper apps are always subject to the most
+ // recent (and possibly most restrictive) target SDK behaviour. Also, this matches the target
+ // SDK of the tests themselves, and of other tests such as CtsNetTestCases.
+ // Note that some of the test helper apps (e.g., CtsHostsideNetworkCapTestsAppSdk33) override
+ // this with older SDK versions.
+ // Also note that unlike android_test targets, "current" does not work: the target SDK is set to
+ // something like "VanillaIceCream" instead of 100000. This means that the tests will not run on
+ // released devices with errors such as "Requires development platform VanillaIceCream but this
+ // is a release platform".
+ target_sdk_version: "10000",
+}
+
java_test_host {
name: "CtsHostsideNetworkTests",
defaults: ["cts_defaults"],
@@ -52,6 +62,6 @@
":CtsHostsideNetworkCapTestsAppWithoutProperty",
":CtsHostsideNetworkCapTestsAppWithProperty",
":CtsHostsideNetworkCapTestsAppSdk33",
- ] + next_app_data,
+ ],
per_testcase_directory: true,
}
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index ea6b078..03ea178 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -20,6 +20,9 @@
<option name="config-descriptor:metadata" key="parameter" value="instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
@@ -46,4 +49,21 @@
<option name="directory-keys" value="/sdcard/CtsHostsideNetworkTests" />
<option name="collect-on-run-ended-only" value="true" />
</metrics_collector>
+
+<!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+ one of the Mainline modules below is present on the device used for testing. -->
+<object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <!-- Tethering Module (internal version). -->
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ <!-- Tethering Module (AOSP version). -->
+ <option name="mainline-module-package-name" value="com.android.tethering" />
+ <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+ <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+ <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+ <option name="mainline-module-package-name" value="com.android.networkstack" />
+ <!-- Resolver Module (internal version). -->
+ <option name="mainline-module-package-name" value="com.google.android.resolv" />
+ <!-- Resolver Module (AOSP version). -->
+ <option name="mainline-module-package-name" value="com.android.resolv" />
+</object>
</configuration>
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 33761dc..31924f0 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -20,6 +20,7 @@
java_test_helper_library {
name: "CtsHostsideNetworkTestsAidl",
sdk_version: "current",
+ min_sdk_version: "30",
srcs: [
"com/android/cts/net/hostside/*.aidl",
"com/android/cts/net/hostside/*.java",
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 919e025..7fff1c2 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -19,9 +19,13 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-java_defaults {
- name: "CtsHostsideNetworkTestsAppDefaults",
- platform_apis: true,
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
+ ],
static_libs: [
"CtsHostsideNetworkTestsAidl",
"androidx.test.ext.junit",
@@ -33,40 +37,10 @@
"modules-utils-build",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
],
srcs: [
"src/**/*.java",
],
- // Tag this module as a cts test artifact
- test_suites: [
- "general-tests",
- "sts",
- ],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkTestsApp",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- "CtsHostsideNetworkTestsAppDefaults",
- ],
- static_libs: [
- "NetworkStackApiStableShims",
- ],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkTestsAppNext",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- "CtsHostsideNetworkTestsAppDefaults",
- "ConnectivityNextEnableDefaults",
- ],
- static_libs: [
- "NetworkStackApiCurrentShims",
- ],
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
index fe522a0..a39a8d0 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -29,9 +29,6 @@
import android.util.Pair;
import com.android.modules.utils.build.SdkLevel;
-import com.android.networkstack.apishim.VpnServiceBuilderShimImpl;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.networkstack.apishim.common.VpnServiceBuilderShim;
import com.android.testutils.PacketReflector;
import java.io.IOException;
@@ -102,8 +99,7 @@
}
private void start(String packageName, Intent intent) {
- Builder builder = new Builder();
- VpnServiceBuilderShim vpnServiceBuilderShim = VpnServiceBuilderShimImpl.newInstance();
+ VpnService.Builder builder = new VpnService.Builder();
final String addresses = parseIpAndMaskListArgument(packageName, intent, "addresses",
builder::addAddress);
@@ -112,11 +108,7 @@
if (SdkLevel.isAtLeastT() && intent.getBooleanExtra(packageName + ".addRoutesByIpPrefix",
false)) {
addedRoutes = parseIpPrefixListArgument(packageName, intent, "routes", (prefix) -> {
- try {
- vpnServiceBuilderShim.addRoute(builder, prefix);
- } catch (UnsupportedApiLevelException e) {
- throw new RuntimeException(e);
- }
+ builder.addRoute(prefix);
});
} else {
addedRoutes = parseIpAndMaskListArgument(packageName, intent, "routes",
@@ -127,11 +119,7 @@
if (SdkLevel.isAtLeastT()) {
excludedRoutes = parseIpPrefixListArgument(packageName, intent, "excludedRoutes",
(prefix) -> {
- try {
- vpnServiceBuilderShim.excludeRoute(builder, prefix);
- } catch (UnsupportedApiLevelException e) {
- throw new RuntimeException(e);
- }
+ builder.excludeRoute(prefix);
});
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index e186c6b..d05a8d0 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -20,8 +20,11 @@
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.content.Context.RECEIVER_EXPORTED;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
@@ -46,11 +49,7 @@
import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_RESUMED;
import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_STARTED;
import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_STOPPED;
-import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
-import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.RecorderCallback.CallbackEntry.BLOCKED_STATUS_INT;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -213,6 +212,8 @@
private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
"automatic_on_off_keepalive_version";
+ private static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+ "ingress_to_vpn_address_filtering";
// Enabled since version 1 means it's always enabled because the version is always above 1
private static final String AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED = "1";
private static final long TEST_TCP_POLLING_TIMER_EXPIRED_PERIOD_MS = 60_000L;
@@ -890,7 +891,7 @@
entry -> entry.getCaps().hasTransport(TRANSPORT_VPN));
}
- @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testChangeUnderlyingNetworks() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
@@ -995,6 +996,13 @@
FIREWALL_CHAIN_BACKGROUND));
otherUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
true /* validated */, isOtherUidBlocked, TIMEOUT_MS);
+ } else {
+ // R does not have per-UID callback or system default callback APIs, and sends an
+ // additional CAP_CHANGED callback.
+ registerDefaultNetworkCallback(myUidCallback);
+ myUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+ true /* validated */, false /* blocked */, TIMEOUT_MS);
+ myUidCallback.expect(CallbackEntry.NETWORK_CAPS_UPDATED, defaultNetwork);
}
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -1136,12 +1144,12 @@
return null;
}
- @Test
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Automatic keepalives were added in U.
public void testAutomaticOnOffKeepaliveModeNoClose() throws Exception {
doTestAutomaticOnOffKeepaliveMode(false);
}
- @Test
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Automatic keepalives were added in U.
public void testAutomaticOnOffKeepaliveModeClose() throws Exception {
doTestAutomaticOnOffKeepaliveMode(true);
}
@@ -1707,7 +1715,8 @@
}
private void maybeExpectVpnTransportInfo(Network network) {
- assumeTrue(SdkLevel.isAtLeastS());
+ // VpnTransportInfo was only added in S.
+ if (!SdkLevel.isAtLeastS()) return;
final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
final TransportInfo ti = vpnNc.getTransportInfo();
@@ -1949,6 +1958,9 @@
*/
private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
throws Exception {
+ assumeTrue(mCM.isConnectivityServiceFeatureEnabledForTesting(
+ INGRESS_TO_VPN_ADDRESS_FILTERING));
+
final NetworkRequest request = new NetworkRequest.Builder()
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 12ea23b..05abcdd 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -21,19 +21,14 @@
android_test_helper_app {
name: "CtsHostsideNetworkTestsApp2",
- defaults: ["cts_support_defaults"],
- platform_apis: true,
+ defaults: [
+ "cts_support_defaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
+ ],
static_libs: [
"androidx.annotation_annotation",
"CtsHostsideNetworkTestsAidl",
- "NetworkStackApiStableShims",
],
srcs: ["src/**/*.java"],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
sdk_version: "test_current",
}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index c220000..0eed51c 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -21,7 +21,6 @@
java_defaults {
name: "CtsHostsideNetworkCapTestsAppDefaults",
- platform_apis: true,
static_libs: [
"androidx.test.ext.junit",
"androidx.test.rules",
@@ -29,12 +28,6 @@
"cts-net-utils",
],
srcs: ["src/**/*.java"],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
}
android_test_helper_app {
@@ -42,6 +35,7 @@
defaults: [
"cts_support_defaults",
"CtsHostsideNetworkCapTestsAppDefaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
],
manifest: "AndroidManifestWithoutProperty.xml",
sdk_version: "test_current",
@@ -52,6 +46,7 @@
defaults: [
"cts_support_defaults",
"CtsHostsideNetworkCapTestsAppDefaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
],
manifest: "AndroidManifestWithProperty.xml",
sdk_version: "test_current",
@@ -62,6 +57,7 @@
defaults: [
"cts_support_defaults",
"CtsHostsideNetworkCapTestsAppDefaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
],
target_sdk_version: "33",
manifest: "AndroidManifestWithoutProperty.xml",
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 69d61b3..e222ff6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -19,7 +19,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
-import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.targetprep.BuildError;
@@ -36,17 +35,13 @@
abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
protected static final String TEST_PKG = "com.android.cts.net.hostside";
protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
- protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
@BeforeClassWithInfo
public static void setUpOnceBase(TestInformation testInfo) throws Exception {
- DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(testInfo.getDevice());
- String testApk = deviceSdkLevel.isDeviceAtLeastV() ? TEST_APK_NEXT : TEST_APK;
-
uninstallPackage(testInfo, TEST_PKG, false);
- installPackage(testInfo, testApk);
+ installPackage(testInfo, TEST_APK);
}
@AfterClassWithInfo
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 fa68e3e..ae572e6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -166,6 +166,8 @@
*/
@Test
public void testRouterSolicitations() throws Exception {
+ assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastU());
+
for (String interfaceDir : mSysctlDirs) {
String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations";
int value = readIntFromPath(path);
@@ -186,8 +188,7 @@
*/
@Test
public void testCongestionControl() throws Exception {
- final DeviceSdkLevel dsl = new DeviceSdkLevel(mDevice);
- assumeTrue(dsl.isDeviceAtLeastV());
+ assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastV());
String path = "/proc/sys/net/ipv4/tcp_congestion_control";
String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 5f9b3ef..40aa1e4 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -19,13 +19,16 @@
python_test_host {
name: "CtsConnectivityMultiDevicesTestCases",
- main: "connectivity_multi_devices_test.py",
+ main: "run_tests.py",
srcs: [
+ "apfv4_test.py",
"connectivity_multi_devices_test.py",
- "tether_utils.py",
+ "run_tests.py",
],
libs: [
+ "absl-py",
"mobly",
+ "net-tests-utils-host-python-common",
],
test_suites: [
"cts",
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
new file mode 100644
index 0000000..7795be5
--- /dev/null
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -0,0 +1,64 @@
+# 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.
+
+from absl.testing import parameterized
+from mobly import asserts
+from net_tests_utils.host.python import apf_test_base, apf_utils
+
+# Constants.
+COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
+ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
+
+
+class ApfV4Test(apf_test_base.ApfTestBase, parameterized.TestCase):
+ def setup_class(self):
+ super().setup_class()
+ # Check apf version preconditions.
+ caps = apf_utils.get_apf_capabilities(
+ self.clientDevice, self.client_iface_name
+ )
+ if self.client.getVsrApiLevel() >= 34:
+ # Enforce APFv4 support for Android 14+ VSR.
+ asserts.assert_true(
+ caps.apf_version_supported >= 4,
+ "APFv4 became mandatory in Android 14 VSR.",
+ )
+ else:
+ # Skip tests for APF version < 4 before Android 14 VSR.
+ apf_utils.assume_apf_version_support_at_least(
+ self.clientDevice, self.client_iface_name, 4
+ )
+
+ # APF L2 packet filtering on V+ Android allows only specific
+ # types: IPv4, ARP, IPv6, EAPOL, WAPI.
+ # Tests can use any disallowed packet type. Currently,
+ # several ethertypes from the legacy ApfFilter denylist are used.
+ @parameterized.parameters(
+ "88a2", # ATA over Ethernet
+ "88a4", # EtherCAT
+ "88b8", # GOOSE (Generic Object Oriented Substation event)
+ "88cd", # SERCOS III
+ "88e3", # Media Redundancy Protocol (IEC62439-2)
+ ) # Declare inputs for state_str and expected_result.
+ def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
+ # Ethernet header (14 bytes).
+ packet = ETHER_BROADCAST_ADDR # Destination MAC (broadcast)
+ packet += self.server_mac_address.replace(":", "") # Source MAC
+ packet += blocked_ether_type
+
+ # Pad with zeroes to minimum ethernet frame length.
+ packet += "00" * 46
+ self.send_packet_and_expect_counter_increased(
+ packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
+ )
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index 1d6fb51..416a2e8 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -1,37 +1,25 @@
-# Lint as: python3
-"""Connectivity multi devices tests."""
-import sys
-from mobly import base_test
-from mobly import test_runner
-from mobly import utils
-from mobly.controllers import android_device
-import tether_utils
-from tether_utils import UpstreamType
+# 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.
-CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python import wifip2p_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
-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,
- )
+class ConnectivityMultiDevicesTest(
+ multi_devices_test_base.MultiDevicesTestBase
+):
def test_hotspot_upstream_wifi(self):
tether_utils.assume_hotspot_test_preconditions(
@@ -61,10 +49,41 @@
self.serverDevice, UpstreamType.CELLULAR
)
+ def test_mdns_via_hotspot(self):
+ tether_utils.assume_hotspot_test_preconditions(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ mdns_utils.assume_mdns_test_preconditions(
+ self.clientDevice, self.serverDevice
+ )
+ try:
+ # Connectivity of the client verified by asserting the validated capability.
+ tether_utils.setup_hotspot_and_client_for_upstream_type(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ mdns_utils.register_mdns_service_and_discover_resolve(
+ self.clientDevice, self.serverDevice
+ )
+ finally:
+ mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+ tether_utils.cleanup_tethering_for_upstream_type(
+ self.serverDevice, UpstreamType.NONE
+ )
-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()
+ def test_mdns_via_wifip2p(self):
+ wifip2p_utils.assume_wifi_p2p_test_preconditions(
+ self.serverDevice, self.clientDevice
+ )
+ mdns_utils.assume_mdns_test_preconditions(
+ self.clientDevice, self.serverDevice
+ )
+ try:
+ wifip2p_utils.setup_wifi_p2p_server_and_client(
+ self.serverDevice, self.clientDevice
+ )
+ mdns_utils.register_mdns_service_and_discover_resolve(
+ self.clientDevice, self.serverDevice
+ )
+ finally:
+ mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+ wifip2p_utils.cleanup_wifi_p2p(self.serverDevice, self.clientDevice)
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
new file mode 100644
index 0000000..1391d13
--- /dev/null
+++ b/tests/cts/multidevices/run_tests.py
@@ -0,0 +1,38 @@
+# 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.
+
+"""Main entrypoint for all of test cases."""
+
+import sys
+from apfv4_test import ApfV4Test
+from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+ # For MoblyBinaryHostTest, this entry point will be called twice:
+ # 1. List tests.
+ # <mobly-par-file-name> -- --list_tests
+ # 2. Run tests.
+ # <mobly-par-file-name> -- --config=<yaml-path> \
+ # --device_serial=<device-serial> --log_path=<log-path>
+ # Strip the "--" since suite runner doesn't recognize it.
+ # While the parameters before "--" is for the infrastructure,
+ # ignore them if any. Also, do not alter parameters if there
+ # is no "--", in case the binary is invoked manually.
+ if "--" in sys.argv:
+ index = sys.argv.index("--")
+ sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+ # TODO: make the tests can be executed without manually list classes.
+ suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
index 5940cbb..c94087e 100644
--- a/tests/cts/multidevices/snippet/Android.bp
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -25,6 +25,8 @@
],
srcs: [
"ConnectivityMultiDevicesSnippet.kt",
+ "MdnsMultiDevicesSnippet.kt",
+ "Wifip2pMultiDevicesSnippet.kt",
],
manifest: "AndroidManifest.xml",
static_libs: [
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
index 9ed8146..4637497 100644
--- a/tests/cts/multidevices/snippet/AndroidManifest.xml
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -21,13 +21,17 @@
<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" />
+ <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
+ android:usesPermissionFlags="neverForLocation" />
<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" />
+ android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet,
+ com.google.snippet.connectivity.MdnsMultiDevicesSnippet,
+ com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet" />
</application>
<!-- Add an instrumentation tag so that the app can be launched through an
instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index f4ad2c4..49688cc 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -21,6 +21,7 @@
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
@@ -35,6 +36,8 @@
import android.net.wifi.WifiNetworkSpecifier
import android.net.wifi.WifiSsid
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.PropertyUtil
+import com.android.modules.utils.build.SdkLevel
import com.android.testutils.AutoReleaseNetworkCallbackRule
import com.android.testutils.ConnectUtil
import com.android.testutils.NetworkCallbackHelper
@@ -70,6 +73,15 @@
@Rpc(description = "Check whether the device supporters AP + STA concurrency.")
fun isStaApConcurrencySupported() = wifiManager.isStaApConcurrencySupported()
+ @Rpc(description = "Check whether the device SDK is as least T")
+ fun isAtLeastT() = SdkLevel.isAtLeastT()
+
+ @Rpc(description = "Return whether the Sdk level is at least V.")
+ fun isAtLeastV() = SdkLevel.isAtLeastV()
+
+ @Rpc(description = "Return the API level that the VSR requirement must be fulfilled.")
+ fun getVsrApiLevel() = PropertyUtil.getVsrApiLevel()
+
@Rpc(description = "Request cellular connection and ensure it is the default network.")
fun requestCellularAndEnsureDefault() {
ctsNetUtils.disableWifi()
@@ -129,6 +141,12 @@
}
}
+ @Rpc(description = "Get interface name from NetworkHandle")
+ fun getInterfaceNameFromNetworkHandle(networkHandle: Long): String {
+ val network = Network.fromNetworkHandle(networkHandle)
+ return cm.getLinkProperties(network)!!.getInterfaceName()!!
+ }
+
@Rpc(description = "Check whether the device supports hotspot feature.")
fun hasHotspotFeature(): Boolean {
val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
@@ -140,7 +158,7 @@
}
@Rpc(description = "Start a hotspot with given SSID and passphrase.")
- fun startHotspot(ssid: String, passphrase: String) {
+ fun startHotspot(ssid: String, passphrase: String): String {
// Store old config.
runAsShell(OVERRIDE_WIFI_CONFIG) {
oldSoftApConfig = wifiManager.getSoftApConfiguration()
@@ -157,7 +175,7 @@
val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
try {
tetheringCallback.expectNoTetheringActive()
- ctsTetheringUtils.startWifiTethering(tetheringCallback)
+ return ctsTetheringUtils.startWifiTethering(tetheringCallback).getInterface()
} finally {
ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
}
diff --git a/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
new file mode 100644
index 0000000..1b288df
--- /dev/null
+++ b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
@@ -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.google.snippet.connectivity
+
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import kotlin.test.assertEquals
+import org.junit.Assert.assertArrayEquals
+
+private const val SERVICE_NAME = "MultiDevicesTest"
+private const val SERVICE_TYPE = "_multi_devices._tcp"
+private const val SERVICE_ATTRIBUTES_KEY = "key"
+private const val SERVICE_ATTRIBUTES_VALUE = "value"
+private const val SERVICE_PORT = 12345
+private const val REGISTRATION_TIMEOUT_MS = 10_000L
+
+class MdnsMultiDevicesSnippet : Snippet {
+ private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+ private val nsdManager = context.getSystemService(NsdManager::class.java)!!
+ private val registrationRecord = NsdRegistrationRecord()
+ private val discoveryRecord = NsdDiscoveryRecord()
+ private val resolveRecord = NsdResolveRecord()
+
+ @Rpc(description = "Register a mDns service")
+ fun registerMDnsService() {
+ val info = NsdServiceInfo()
+ info.setServiceName(SERVICE_NAME)
+ info.setServiceType(SERVICE_TYPE)
+ info.setPort(SERVICE_PORT)
+ info.setAttribute(SERVICE_ATTRIBUTES_KEY, SERVICE_ATTRIBUTES_VALUE)
+ nsdManager.registerService(info, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+ registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+ }
+
+ @Rpc(description = "Unregister a mDns service")
+ fun unregisterMDnsService() {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+
+ @Rpc(description = "Ensure the discovery and resolution of the mDNS service")
+ // Suppress the warning, as the NsdManager#resolveService() method is deprecated.
+ @Suppress("DEPRECATION")
+ fun ensureMDnsServiceDiscoveryAndResolution() {
+ // Discover a mDns service that matches the test service
+ nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+ val info = discoveryRecord.waitForServiceDiscovered(SERVICE_NAME, SERVICE_TYPE)
+ // Resolve the retrieved mDns service.
+ nsdManager.resolveService(info, resolveRecord)
+ val serviceResolved = resolveRecord.expectCallbackEventually<ServiceResolved>()
+ serviceResolved.serviceInfo.let {
+ assertEquals(SERVICE_NAME, it.serviceName)
+ assertEquals(".$SERVICE_TYPE", it.serviceType)
+ assertEquals(SERVICE_PORT, it.port)
+ assertEquals(1, it.attributes.size)
+ assertArrayEquals(
+ SERVICE_ATTRIBUTES_VALUE.encodeToByteArray(),
+ it.attributes[SERVICE_ATTRIBUTES_KEY]
+ )
+ }
+ }
+
+ @Rpc(description = "Stop discovery")
+ fun stopMDnsServiceDiscovery() {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+ }
+}
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
new file mode 100644
index 0000000..f8c9351
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -0,0 +1,312 @@
+/*
+ * 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.MANAGE_WIFI_NETWORK_SELECTION
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.MacAddress
+import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pConfig
+import android.net.wifi.p2p.WifiP2pDevice
+import android.net.wifi.p2p.WifiP2pDeviceList
+import android.net.wifi.p2p.WifiP2pGroup
+import android.net.wifi.p2p.WifiP2pManager
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+
+private const val TIMEOUT_MS = 60000L
+
+class Wifip2pMultiDevicesSnippet : Snippet {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() }
+ private val wifiManager by lazy {
+ context.getSystemService(WifiManager::class.java)
+ ?: fail("Could not get WifiManager service")
+ }
+ private val wifip2pManager by lazy {
+ context.getSystemService(WifiP2pManager::class.java)
+ ?: fail("Could not get WifiP2pManager service")
+ }
+ private lateinit var wifip2pChannel: WifiP2pManager.Channel
+ private val wifip2pIntentReceiver = Wifip2pIntentReceiver()
+
+ private class Wifip2pIntentReceiver : BroadcastReceiver() {
+ val history = ArrayTrackRecord<IntentReceivedEvent>().newReadHead()
+
+ sealed class IntentReceivedEvent {
+ abstract val intent: Intent
+ data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent()
+ data class PeersChanged(override val intent: Intent) : IntentReceivedEvent()
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
+ history.add(ConnectionChanged(intent))
+ }
+ WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
+ history.add(PeersChanged(intent))
+ }
+ }
+ }
+
+ inline fun <reified T : IntentReceivedEvent> eventuallyExpectedIntent(
+ timeoutMs: Long = TIMEOUT_MS,
+ crossinline predicate: (T) -> Boolean = { true }
+ ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also {
+ assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.")
+ } as T
+ }
+
+ @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
+ fun isP2pSupported() = wifiManager.isP2pSupported()
+
+ @Rpc(description = "Start Wi-Fi P2P")
+ fun startWifiP2p() {
+ // Initialize Wi-Fi P2P
+ wifip2pChannel = wifip2pManager.initialize(context, context.mainLooper, null)
+
+ // Ensure the Wi-Fi P2P channel is available
+ val p2pStateEnabledFuture = CompletableFuture<Boolean>()
+ wifip2pManager.requestP2pState(wifip2pChannel) { state ->
+ if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
+ p2pStateEnabledFuture.complete(true)
+ }
+ }
+ p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ // Register an intent filter to receive Wi-Fi P2P intents
+ val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
+ filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
+ context.registerReceiver(wifip2pIntentReceiver, filter)
+ }
+
+ @Rpc(description = "Stop Wi-Fi P2P")
+ fun stopWifiP2p() {
+ if (this::wifip2pChannel.isInitialized) {
+ wifip2pManager.cancelConnect(wifip2pChannel, null)
+ wifip2pManager.removeGroup(wifip2pChannel, null)
+ }
+ // Unregister the intent filter
+ context.unregisterReceiver(wifip2pIntentReceiver)
+ }
+
+ @Rpc(description = "Get the current device name")
+ fun getDeviceName(): String {
+ // Retrieve current device info
+ val deviceFuture = CompletableFuture<String>()
+ wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice ->
+ if (wifiP2pDevice != null) {
+ deviceFuture.complete(wifiP2pDevice.deviceName)
+ }
+ }
+ // Return current device name
+ return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ @Rpc(description = "Wait for a p2p connection changed intent and check the group")
+ @Suppress("DEPRECATION")
+ fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) {
+ wifip2pIntentReceiver.eventuallyExpectedIntent<ConnectionChanged>() {
+ val p2pGroup: WifiP2pGroup? =
+ it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
+ val groupMatched = p2pGroup?.networkName == groupName
+ return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched
+ }
+ }
+
+ @Rpc(description = "Create a Wi-Fi P2P group")
+ fun createGroup(groupName: String, groupPassphrase: String) {
+ // Create a Wi-Fi P2P group
+ val wifip2pConfig = WifiP2pConfig.Builder()
+ .setNetworkName(groupName)
+ .setPassphrase(groupPassphrase)
+ .build()
+ val createGroupFuture = CompletableFuture<Boolean>()
+ wifip2pManager.createGroup(
+ wifip2pChannel,
+ wifip2pConfig,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() { createGroupFuture.complete(true) }
+ }
+ )
+ createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+ // Ensure the Wi-Fi P2P group is created.
+ waitForP2pConnectionChanged(false, groupName)
+ }
+
+ @Rpc(description = "Start Wi-Fi P2P peers discovery")
+ fun startPeersDiscovery() {
+ // Start discovery Wi-Fi P2P peers
+ wifip2pManager.discoverPeers(wifip2pChannel, null)
+
+ // Ensure the discovery is started
+ val p2pDiscoveryStartedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.requestDiscoveryState(wifip2pChannel) { state ->
+ if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
+ p2pDiscoveryStartedFuture.complete(true)
+ }
+ }
+ p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ /**
+ * Get the device address from the given intent that matches the given device name.
+ *
+ * @param peersChangedIntent the intent to get the device address from
+ * @param deviceName the target device name
+ * @return the address of the target device or null if no devices match.
+ */
+ @Suppress("DEPRECATION")
+ private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? {
+ val peers: WifiP2pDeviceList? =
+ peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
+ return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress
+ }
+
+ /**
+ * Ensure the given device has been discovered and returns the associated device address for
+ * connection.
+ *
+ * @param deviceName the target device name
+ * @return the address of the target device.
+ */
+ @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered")
+ fun ensureDeviceDiscovered(deviceName: String): String {
+ val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent<PeersChanged>() {
+ return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null
+ }
+ return getDeviceAddress(changedEvent.intent, deviceName)
+ ?: fail("Missing device in filtered intent")
+ }
+
+ @Rpc(description = "Invite a Wi-Fi P2P device to the group")
+ fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) {
+ // Connect to the device to send invitation
+ val wifip2pConfig = WifiP2pConfig.Builder()
+ .setNetworkName(groupName)
+ .setPassphrase(groupPassphrase)
+ .setDeviceAddress(MacAddress.fromString(deviceAddress))
+ .build()
+ val connectedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.connect(
+ wifip2pChannel,
+ wifip2pConfig,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() {
+ connectedFuture.complete(true)
+ }
+ }
+ )
+ connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ private fun runExternalApproverForGroupProcess(
+ deviceAddress: String,
+ isGroupInvitation: Boolean
+ ) {
+ val peer = MacAddress.fromString(deviceAddress)
+ runAsShell(MANAGE_WIFI_NETWORK_SELECTION) {
+ val connectionRequestFuture = CompletableFuture<Boolean>()
+ val attachedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.addExternalApprover(
+ wifip2pChannel,
+ peer,
+ object : WifiP2pManager.ExternalApproverRequestListener {
+ override fun onAttached(deviceAddress: MacAddress) {
+ attachedFuture.complete(true)
+ }
+ override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit
+ override fun onConnectionRequested(
+ requestType: Int,
+ config: WifiP2pConfig,
+ device: WifiP2pDevice
+ ) {
+ connectionRequestFuture.complete(true)
+ }
+ override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit
+ }
+ )
+ if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else
+ connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+ val resultFuture = CompletableFuture<Boolean>()
+ wifip2pManager.setConnectionRequestResult(
+ wifip2pChannel,
+ peer,
+ WifiP2pManager.CONNECTION_REQUEST_ACCEPT,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() {
+ resultFuture.complete(true)
+ }
+ }
+ )
+ resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+ val removeFuture = CompletableFuture<Boolean>()
+ wifip2pManager.removeExternalApprover(
+ wifip2pChannel,
+ peer,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() {
+ removeFuture.complete(true)
+ }
+ }
+ )
+ removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+ }
+
+ @Rpc(description = "Accept P2P group invitation from device")
+ fun acceptGroupInvitation(deviceAddress: String) {
+ // Accept the Wi-Fi P2P group invitation
+ runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */)
+ }
+
+ @Rpc(description = "Wait for connection request from the peer and accept joining")
+ fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) {
+ // Wait for connection request from the peer and accept joining
+ runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */)
+ }
+
+ @Rpc(description = "Ensure the target device is connected")
+ fun ensureDeviceConnected(deviceName: String) {
+ // Retrieve peers and ensure the target device is connected
+ val connectedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any {
+ it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let {
+ connectedFuture.complete(true)
+ }
+ }
+ connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index ae85701..a5ad7f2 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -29,7 +29,7 @@
libs: [
"voip-common",
- "android.test.base",
+ "android.test.base.stubs",
],
jni_libs: [
@@ -111,25 +111,6 @@
min_sdk_version: "30",
}
-// Networking CTS tests that target the latest released SDK. These tests can be installed on release
-// devices at any point in the Android release cycle and are useful for qualifying mainline modules
-// on release devices.
-android_test {
- name: "CtsNetTestCasesLatestSdk",
- defaults: [
- "ConnectivityTestsLatestSdkDefaults",
- "CtsNetTestCasesDefaults",
- "CtsNetTestCasesApiStableDefaults",
- ],
- test_suites: [
- "general-tests",
- "mts-dnsresolver",
- "mts-networking",
- "mts-tethering",
- "mts-wifi",
- ],
-}
-
java_defaults {
name: "CtsNetTestCasesMaxTargetSdkDefaults",
defaults: [
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 077c3ef..24431a6 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -25,6 +25,7 @@
<option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
+ <option name="has-server-side-config" value="false" />
<option name="target" value="device" />
<option name="config-filename" value="{MODULE}" />
<option name="version" value="1.0" />
@@ -48,8 +49,8 @@
those tests with an annotation matching the name of the APK.
This allows us to maintain one AndroidTestTemplate.xml for all CtsNetTestCases*.apk,
- and have CtsNetTestCases and CtsNetTestCasesLatestSdk run all tests, but have
- CtsNetTestCasesMaxTargetSdk31 run only tests that require target SDK 31.
+ and have CtsNetTestCases run all tests, but have CtsNetTestCasesMaxTargetSdk31 run only
+ tests that require target SDK 31.
This relies on the fact that if the class specified in include-annotation exists, then
the runner will only run the tests annotated with that annotation, but if it does not,
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 587d5a5..7d93c3a 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -25,7 +25,7 @@
compile_multilib: "both",
libs: [
- "android.test.base",
+ "android.test.base.stubs.test",
],
srcs: [
diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml
index 8042d50..fcc73f3 100644
--- a/tests/cts/net/api23Test/AndroidTest.xml
+++ b/tests/cts/net/api23Test/AndroidTest.xml
@@ -18,6 +18,7 @@
<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" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 764f117..9ac2c67 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -27,9 +27,20 @@
import android.net.NetworkRequest
import android.net.apf.ApfCapabilities
import android.net.apf.ApfConstants.ETH_ETHERTYPE_OFFSET
+import android.net.apf.ApfConstants.ETH_HEADER_LEN
+import android.net.apf.ApfConstants.ICMP6_CHECKSUM_OFFSET
import android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET
+import android.net.apf.ApfConstants.IPV6_DEST_ADDR_OFFSET
+import android.net.apf.ApfConstants.IPV6_HEADER_LEN
import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
+import android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET
+import android.net.apf.ApfCounterTracker
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING
+import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
import android.net.apf.ApfV4Generator
+import android.net.apf.ApfV4GeneratorBase
+import android.net.apf.ApfV6Generator
import android.net.apf.BaseApfGenerator
import android.net.apf.BaseApfGenerator.MemorySlot
import android.net.apf.BaseApfGenerator.Register.R0
@@ -57,6 +68,12 @@
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.compatibility.common.util.VsrTest
import com.android.internal.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN
+import com.android.net.module.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_HEADER_MIN_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.PacketReader
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -73,7 +90,6 @@
import com.google.common.truth.Truth.assertWithMessage
import com.google.common.truth.TruthJUnit.assume
import java.io.FileDescriptor
-import java.lang.Thread
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.util.concurrent.CompletableFuture
@@ -213,8 +229,8 @@
Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
}
- fun expectPingReply(): ByteArray {
- return futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): ByteArray {
+ return futureReply!!.get(timeoutMs, TimeUnit.MILLISECONDS)
}
fun expectPingDropped() {
@@ -394,7 +410,13 @@
}
}
- fun ApfV4Generator.addPassIfNotIcmpv6EchoReply() {
+ private fun installAndVerifyProgram(program: ByteArray) {
+ installProgram(program)
+ val readResult = readProgram().take(program.size).toByteArray()
+ assertThat(readResult).isEqualTo(program)
+ }
+
+ fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
// If not IPv6 -> PASS
addLoad16(R0, ETH_ETHERTYPE_OFFSET)
addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
@@ -434,7 +456,11 @@
assertThat(packetReader.expectPingReply()).isEqualTo(data)
// Generate an APF program that drops the next ping
- val gen = ApfV4Generator(4)
+ val gen = ApfV4Generator(
+ caps.apfVersionSupported,
+ caps.maximumApfProgramSize,
+ caps.maximumApfProgramSize
+ )
// If not ICMPv6 Echo Reply -> PASS
gen.addPassIfNotIcmpv6EchoReply()
@@ -447,8 +473,7 @@
gen.addJump(BaseApfGenerator.DROP_LABEL)
val program = gen.generate()
- installProgram(program)
- readProgram() // wait for install completion
+ installAndVerifyProgram(program)
packetReader.sendPing(data, payloadSize)
packetReader.expectPingDropped()
@@ -468,7 +493,11 @@
// Test v4 memory slots on both v4 and v6 interpreters.
assumeApfVersionSupportAtLeast(4)
clearApfMemory()
- val gen = ApfV4Generator(4)
+ val gen = ApfV4Generator(
+ caps.apfVersionSupported,
+ caps.maximumApfProgramSize,
+ caps.maximumApfProgramSize
+ )
// If not ICMPv6 Echo Reply -> PASS
gen.addPassIfNotIcmpv6EchoReply()
@@ -489,8 +518,7 @@
val program = gen.generate()
assertThat(program.size).isLessThan(counterRegion)
- installProgram(program)
- readProgram() // wait for install completion
+ installAndVerifyProgram(program)
// Trigger the program by sending a ping and waiting on the reply.
val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
@@ -523,7 +551,11 @@
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
clearApfMemory()
- val gen = ApfV4Generator(4)
+ val gen = ApfV4Generator(
+ caps.apfVersionSupported,
+ caps.maximumApfProgramSize,
+ caps.maximumApfProgramSize
+ )
// If not ICMPv6 Echo Reply -> PASS
gen.addPassIfNotIcmpv6EchoReply()
@@ -534,8 +566,7 @@
gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
gen.addStoreData(R0, 0)
- installProgram(gen.generate())
- readProgram() // wait for install completion
+ installAndVerifyProgram(gen.generate())
val payloadSize = 56
val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
@@ -556,4 +587,147 @@
val timeDiff = filterAgeSeconds - filterAgeSecondsOrig
assertThat(timeDiff).isAnyOf(5, 6)
}
+
+ @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+ @Test
+ fun testFilterAge16384thsIncreasesBetweenPackets() {
+ assumeApfVersionSupportAtLeast(6000)
+ clearApfMemory()
+ val gen = ApfV6Generator(
+ caps.apfVersionSupported,
+ caps.maximumApfProgramSize,
+ caps.maximumApfProgramSize
+ )
+
+ // If not ICMPv6 Echo Reply -> PASS
+ gen.addPassIfNotIcmpv6EchoReply()
+
+ // Store all prefilled memory slots in counter region [500, 520)
+ gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS)
+ gen.addStoreCounter(FILTER_AGE_16384THS, R0)
+
+ installAndVerifyProgram(gen.generate())
+
+ val payloadSize = 56
+ val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+ packetReader.sendPing(data, payloadSize)
+ packetReader.expectPingReply()
+
+ var apfRam = readProgram()
+ val filterAge16384thSecondsOrig =
+ ApfCounterTracker.getCounterValue(apfRam, FILTER_AGE_16384THS)
+
+ Thread.sleep(5000)
+
+ packetReader.sendPing(data, payloadSize)
+ packetReader.expectPingReply()
+
+ apfRam = readProgram()
+ val filterAge16384thSeconds = ApfCounterTracker.getCounterValue(apfRam, FILTER_AGE_16384THS)
+ val timeDiff = (filterAge16384thSeconds - filterAge16384thSecondsOrig)
+ // Expect the HAL plus ping latency to be less than 800ms.
+ val timeDiffLowerBound = (4.99 * 16384).toInt()
+ val timeDiffUpperBound = (5.81 * 16384).toInt()
+ // Assert that filter age has increased, but not too much.
+ assertThat(timeDiff).isGreaterThan(timeDiffLowerBound)
+ assertThat(timeDiff).isLessThan(timeDiffUpperBound)
+ }
+
+ @VsrTest(
+ requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005", "VSR-5.3.12-012", "VSR-5.3.12-013",
+ "VSR-5.3.12-014", "VSR-5.3.12-015", "VSR-5.3.12-016", "VSR-5.3.12-017"]
+ )
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testReplyPing() {
+ assumeApfVersionSupportAtLeast(6000)
+ installProgram(ByteArray(caps.maximumApfProgramSize) { 0 }) // Clear previous program
+ readProgram() // Ensure installation is complete
+
+ val payloadSize = 56
+ val payload = ByteArray(payloadSize).also { Random.nextBytes(it) }
+ val firstByte = payload.take(1).toByteArray()
+
+ val pingRequestIpv6PayloadLen = PING_HEADER_LENGTH + 1
+ val pingRequestPktLen = ETH_HEADER_LEN + IPV6_HEADER_LEN + pingRequestIpv6PayloadLen
+
+ val gen = ApfV6Generator(
+ caps.apfVersionSupported,
+ caps.maximumApfProgramSize,
+ caps.maximumApfProgramSize
+ )
+ val skipPacketLabel = gen.uniqueLabel
+
+ // Summary of the program:
+ // if the packet is not ICMPv6 echo reply
+ // pass
+ // else if the echo reply payload size is 1
+ // increase PASSED_IPV6_ICMP counter
+ // pass
+ // else
+ // transmit a ICMPv6 echo request packet with the first byte of the payload in the reply
+ // increase DROPPED_IPV6_MULTICAST_PING counter
+ // drop
+ val program = gen
+ .addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ .addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
+ .addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+ .addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
+ .addLoad8(R0, ICMP6_TYPE_OFFSET)
+ .addJumpIfR0NotEquals(0x81, skipPacketLabel) // Echo reply type
+ .addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+ .addCountAndPassIfR0Equals(
+ (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
+ .toLong(),
+ PASSED_IPV6_ICMP
+ )
+ // Ping Packet Generation
+ .addAllocate(pingRequestPktLen)
+ // Eth header
+ .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
+ .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
+ .addWriteU16(ETH_P_IPV6) // IPv6 type
+ // IPv6 Header
+ .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
+ // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
+ .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
+ .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
+ .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
+ // ICMPv6
+ .addWriteU8(0x80) // type: echo request
+ .addWriteU8(0) // code
+ .addWriteU16(pingRequestIpv6PayloadLen) // checksum
+ // identifier
+ .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
+ .addWriteU16(0) // sequence number
+ .addDataCopy(firstByte) // data
+ .addTransmitL4(
+ ETHER_HEADER_LEN, // ip_ofs
+ ICMP6_CHECKSUM_OFFSET, // csum_ofs
+ IPV6_SRC_ADDR_OFFSET, // csum_start
+ IPPROTO_ICMPV6, // partial_sum
+ false // udp
+ )
+ // Warning: the program abuse DROPPED_IPV6_MULTICAST_PING for debugging purpose
+ .addCountAndDrop(DROPPED_IPV6_MULTICAST_PING)
+ .defineLabel(skipPacketLabel)
+ .addPass()
+ .generate()
+
+ installAndVerifyProgram(program)
+
+ packetReader.sendPing(payload, payloadSize)
+
+ val replyPayload = try {
+ packetReader.expectPingReply(TIMEOUT_MS * 2)
+ } catch (e: TimeoutException) {
+ byteArrayOf() // Empty payload if timeout occurs
+ }
+
+ val apfCounterTracker = ApfCounterTracker()
+ apfCounterTracker.updateCountersFromData(readProgram())
+ Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
+
+ assertThat(replyPayload).isEqualTo(firstByte)
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 07e2024..1389be7 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -44,7 +44,6 @@
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.AndroidJUnit4
-import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
import com.android.testutils.AutoReleaseNetworkCallbackRule
@@ -201,10 +200,7 @@
"access."
assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
- val startPortalAppPermission =
- if (isAtLeastR()) NETWORK_SETTINGS
- else CONNECTIVITY_INTERNAL
- runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
+ runAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
// Expect the portal content to be fetched at some point after detecting the portal.
// Some implementations may fetch the URL before startCaptivePortalApp is called.
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 60869b6..b62db04 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -195,7 +195,6 @@
import androidx.test.filters.RequiresDevice;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.DynamicConfigDeviceSide;
import com.android.internal.util.ArrayUtils;
@@ -207,15 +206,16 @@
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
import com.android.testutils.AutoReleaseNetworkCallbackRule;
import com.android.testutils.CompatUtil;
+import com.android.testutils.ConnectUtil;
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.DeviceInfoUtils;
import com.android.testutils.DumpTestUtils;
import com.android.testutils.RecorderCallback.CallbackEntry;
-import com.android.testutils.SkipMainlinePresubmit;
import com.android.testutils.SkipPresubmit;
import com.android.testutils.TestHttpServer;
import com.android.testutils.TestNetworkTracker;
@@ -275,7 +275,8 @@
import fi.iki.elonen.NanoHTTPD.Response.IStatus;
import fi.iki.elonen.NanoHTTPD.Response.Status;
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
public class ConnectivityManagerTest {
@Rule(order = 1)
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -1054,8 +1055,7 @@
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
@Test @IgnoreUpTo(Build.VERSION_CODES.Q)
- @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
- public void testIsPrivateDnsBroken() throws InterruptedException {
+ public void testIsPrivateDnsBroken() throws Exception {
final String invalidPrivateDnsServer = "invalidhostname.example.com";
final String goodPrivateDnsServer = "dns.google";
mCtsNetUtils.storePrivateDnsSetting();
@@ -1077,11 +1077,9 @@
.isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
} finally {
mCtsNetUtils.restorePrivateDnsSetting();
- // Toggle network to make sure it is re-validated
- mCm.reportNetworkConnectivity(networkForPrivateDns, true);
- cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
- entry -> !(((CallbackEntry.CapabilitiesChanged) entry).getCaps()
- .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+ // Toggle networks to make sure they are re-validated
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
}
}
@@ -2943,20 +2941,10 @@
// This may also apply to wifi in principle, but in practice methods that mock validation
// URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
if (mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
- ensureValidatedNetwork(makeCellNetworkRequest());
+ new ConnectUtil(mContext).ensureCellularValidated();
}
}
- private void ensureValidatedNetwork(NetworkRequest request) {
- final TestableNetworkCallback cb = new TestableNetworkCallback();
- mCm.registerNetworkCallback(request, cb);
- cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
- NETWORK_CALLBACK_TIMEOUT_MS,
- entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
- .hasCapability(NET_CAPABILITY_VALIDATED));
- mCm.unregisterNetworkCallback(cb);
- }
-
@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
@Test
public void testAcceptPartialConnectivity_validatedNetwork() throws Exception {
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 752891f..fa44ae9 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -852,13 +852,14 @@
}
public void doTestContinuousQueries(Executor executor) throws InterruptedException {
- final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN;
for (Network network : getTestableNetworks()) {
for (int i = 0; i < QUERY_TIMES ; ++i) {
- final VerifyCancelInetAddressCallback callback =
- new VerifyCancelInetAddressCallback(msg, null);
// query v6/v4 in turn
boolean queryV6 = (i % 2 == 0);
+ final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN
+ + " on " + network + ", queryV6=" + queryV6;
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A,
FLAG_NO_CACHE_LOOKUP, executor, null, callback);
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index fb63a19..b1e5680 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -16,6 +16,10 @@
package android.net.cts;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
@@ -23,10 +27,16 @@
import android.net.Network;
import android.net.NetworkInfo;
import android.os.SystemClock;
-import android.test.AndroidTestCase;
import android.util.Log;
import androidx.test.filters.RequiresDevice;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -36,7 +46,9 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-public class DnsTest extends AndroidTestCase {
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+public class DnsTest {
static {
System.loadLibrary("nativedns_jni");
@@ -46,10 +58,13 @@
private static final String TAG = "DnsTest";
private static final String PROXY_NETWORK_TYPE = "PROXY";
+ private Context mContext;
private ConnectivityManager mCm;
+ @Before
public void setUp() {
- mCm = getContext().getSystemService(ConnectivityManager.class);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mCm = mContext.getSystemService(ConnectivityManager.class);
}
/**
@@ -69,6 +84,7 @@
* Perf - measure size of first and second tier caches and their effect
* Assert requires network permission
*/
+ @Test
@RequiresDevice // IPv6 support may be missing on presubmit virtual hardware
public void testDnsWorks() throws Exception {
ensureIpv6Connectivity();
@@ -91,7 +107,7 @@
// Skip the rest of the test if the active network for watch is PROXY.
// TODO: Check NetworkInfo type in addition to type name once ag/601257 is merged.
- if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
&& activeNetworkInfoIsProxy()) {
Log.i(TAG, "Skipping test because the active network type name is PROXY.");
return;
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index f73134a..041e6cb 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -298,7 +298,8 @@
fun sendPacket(
agent: TestableNetworkAgent,
sendV6: Boolean,
- dstPort: Int = 0
+ dstPort: Int = 0,
+ times: Int = 1
) {
val testString = "test string"
val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
@@ -308,9 +309,11 @@
IPPROTO_UDP)
checkNotNull(agent.network).bindSocket(socket)
- val originalPacket = testPacket.readAsArray()
- Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+ val origPacket = testPacket.readAsArray()
+ repeat(times) {
+ Os.sendto(socket, origPacket, 0 /* bytesOffset */, origPacket.size, 0 /* flags */,
if (sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
+ }
Os.close(socket)
}
@@ -400,10 +403,11 @@
agent: TestableNetworkAgent,
sendV6: Boolean = false,
dscpValue: Int = 0,
- dstPort: Int = 0
+ dstPort: Int = 0,
+ times: Int = 1
) {
- var packetFound = false
- sendPacket(agent, sendV6, dstPort)
+ var packetFound = 0
+ sendPacket(agent, sendV6, dstPort, times)
// TODO: grab source port from socket in sendPacket
Log.e(TAG, "find DSCP value:" + dscpValue)
@@ -424,10 +428,23 @@
if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
Log.e(TAG, "DSCP value found")
assertEquals(dscpValue, dscp)
- packetFound = true
+ packetFound++
}
}
- assertTrue(packetFound)
+ assertTrue(packetFound == times)
+ }
+
+ fun validatePackets(
+ agent: TestableNetworkAgent,
+ sendV6: Boolean = false,
+ dscpValue: Int = 0,
+ dstPort: Int = 0
+ ) {
+ // We send two packets from the same socket to verify
+ // socket caching works correctly.
+ validatePacket(agent, sendV6, dscpValue, dstPort, 2)
+ // Try one more time from a different socket.
+ validatePacket(agent, sendV6, dscpValue, dstPort, 1)
}
fun doRemovePolicyTest(
@@ -453,10 +470,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
- // Send a second packet to validate that the stored BPF policy
- // is correct for subsequent packets.
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -475,7 +489,7 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, dscpValue = 4, dstPort = 5555)
+ validatePackets(agent, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -494,10 +508,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
- // Send a second packet to validate that the stored BPF policy
- // is correct for subsequent packets.
- validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, true, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -515,7 +526,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
+ validatePackets(agent, true, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -533,7 +544,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -541,7 +552,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -549,16 +560,16 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
doRemovePolicyTest(agent, callback, 1)
- validatePacket(agent, dscpValue = 0, dstPort = 1111)
+ validatePackets(agent, dscpValue = 0, dstPort = 1111)
doRemovePolicyTest(agent, callback, 2)
- validatePacket(agent, dscpValue = 0, dstPort = 2222)
+ validatePackets(agent, dscpValue = 0, dstPort = 2222)
doRemovePolicyTest(agent, callback, 3)
- validatePacket(agent, dscpValue = 0, dstPort = 3333)
+ validatePackets(agent, dscpValue = 0, dstPort = 3333)
}
@Test
@@ -569,7 +580,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
doRemovePolicyTest(agent, callback, 1)
@@ -578,7 +589,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
doRemovePolicyTest(agent, callback, 2)
@@ -587,7 +598,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
doRemovePolicyTest(agent, callback, 3)
}
@@ -601,7 +612,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -609,7 +620,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -617,7 +628,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
@@ -643,7 +654,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1)
@@ -652,7 +663,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1)
@@ -661,24 +672,24 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
agent.sendRemoveAllDscpPolicies()
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 1111)
+ validatePackets(agent, false, dstPort = 1111)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 2222)
+ validatePackets(agent, false, dstPort = 2222)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 3333)
+ validatePackets(agent, false, dstPort = 3333)
}
}
@@ -690,7 +701,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 4444)
}
val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
@@ -700,8 +711,8 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
// Sending packet with old policy should fail
- validatePacket(agent, dscpValue = 0, dstPort = 4444)
- validatePacket(agent, dscpValue = 1, dstPort = 5555)
+ validatePackets(agent, dscpValue = 0, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 5555)
}
agent.sendRemoveDscpPolicy(1)
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index f6a025a..cb55bd5 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -184,6 +184,8 @@
// Static state to reduce setup/teardown
private static final Context sContext = InstrumentationRegistry.getContext();
+ private static boolean sIsWatch =
+ sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
private static final ConnectivityManager sCM =
(ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
private static final VpnManager sVpnMgr =
@@ -205,12 +207,15 @@
@Before
public void setUp() {
- assumeFalse("Skipping test because watches don't support VPN",
- sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
+ assumeFalse("Skipping test because watches don't support VPN", sIsWatch);
}
@After
public void tearDown() {
+ if (sIsWatch) {
+ return; // Tests are skipped for watches.
+ }
+
for (TestableNetworkCallback callback : mCallbacksToUnregister) {
sCM.unregisterNetworkCallback(callback);
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 2a6c638..c480135 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -23,10 +23,13 @@
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.net.cts.PacketUtils.ICMP_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
import static android.system.OsConstants.FIONREAD;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.net.ConnectivityManager;
@@ -152,10 +155,17 @@
final IpSecTransformState transformState =
futureIpSecTransform.get(SOCK_TIMEOUT, TimeUnit.MILLISECONDS);
- assertEquals(txHighestSeqNum, transformState.getTxHighestSequenceNumber());
+ // There might be ICMPv6(Router Solicitation) packets. Thus we can only check the lower
+ // bound of the outgoing traffic.
+ final long icmpV6RsCnt = transformState.getTxHighestSequenceNumber() - txHighestSeqNum;
+ assertTrue(icmpV6RsCnt >= 0);
+
+ final long adjustedPacketCnt = packetCnt + icmpV6RsCnt;
+ final long adjustedByteCnt = byteCnt + icmpV6RsCnt * (IP6_HDRLEN + ICMP_HDRLEN);
+
+ assertEquals(adjustedPacketCnt, transformState.getPacketCount());
+ assertEquals(adjustedByteCnt, transformState.getByteCount());
assertEquals(rxHighestSeqNum, transformState.getRxHighestSequenceNumber());
- assertEquals(packetCnt, transformState.getPacketCount());
- assertEquals(byteCnt, transformState.getByteCount());
assertArrayEquals(replayBitmap, transformState.getReplayBitmap());
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index b5f43d3..e94d94f 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -451,9 +451,10 @@
long uidTxDelta = 0;
long uidRxDelta = 0;
for (int i = 0; i < 100; i++) {
- // Clear TrafficStats cache is needed to avoid rate-limit caching for
- // TrafficStats API results on V+ devices.
- if (SdkLevel.isAtLeastV()) {
+ // Since the target SDK of this test should always equal or be larger than V,
+ // TrafficStats caching is always enabled. Clearing the cache is needed to
+ // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+ if (SdkLevel.isAtLeastT()) {
runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
}
uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
@@ -530,9 +531,10 @@
}
private static void initStatsChecker() throws Exception {
- // Clear TrafficStats cache is needed to avoid rate-limit caching for
- // TrafficStats API results on V+ devices.
- if (SdkLevel.isAtLeastV()) {
+ // Since the target SDK of this test should always equal or be larger than V,
+ // TrafficStats caching is always enabled. Clearing the cache is needed to
+ // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+ if (SdkLevel.isAtLeastT()) {
runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
}
uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
index 22a51d6..890c071 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -65,6 +65,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.CollectionUtils;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -119,7 +120,7 @@
private static final int TIMEOUT_MS = 500;
- private static final int PACKET_COUNT = 5000;
+ private static final int PACKET_COUNT = 100;
// Static state to reduce setup/teardown
private static ConnectivityManager sCM;
@@ -1088,6 +1089,27 @@
UdpEncapsulationSocket encapSocket,
IpSecTunnelTestRunnable test)
throws Exception {
+ return buildTunnelNetworkAndRunTests(
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ encapSocket,
+ test,
+ true /* enableEncrypt */);
+ }
+
+ private int buildTunnelNetworkAndRunTests(
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ int spi,
+ UdpEncapsulationSocket encapSocket,
+ IpSecTunnelTestRunnable test,
+ boolean enableEncrypt)
+ throws Exception {
int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN;
TestNetworkCallback testNetworkCb = null;
int innerSocketPort;
@@ -1115,8 +1137,12 @@
// Configure Transform parameters
IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
- transformBuilder.setEncryption(
- new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+
+ if (enableEncrypt) {
+ transformBuilder.setEncryption(
+ new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+ }
+
transformBuilder.setAuthentication(
new IpSecAlgorithm(
IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
@@ -1167,8 +1193,8 @@
return innerSocketPort;
}
- private int buildTunnelNetworkAndRunTestsSimple(int spi, IpSecTunnelTestRunnable test)
- throws Exception {
+ private int buildTunnelNetworkAndRunTestsSimple(
+ int spi, IpSecTunnelTestRunnable test, boolean enableEncrypt) throws Exception {
return buildTunnelNetworkAndRunTests(
LOCAL_INNER_6,
REMOTE_INNER_6,
@@ -1176,7 +1202,8 @@
REMOTE_OUTER_6,
spi,
null /* encapSocket */,
- test);
+ test,
+ enableEncrypt);
}
private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
@@ -1787,10 +1814,11 @@
PACKET_COUNT,
PACKET_COUNT,
PACKET_COUNT * (long) innerPacketSize,
- newReplayBitmap(REPLAY_BITMAP_LEN_BYTE * 8));
+ newReplayBitmap(PACKET_COUNT));
return innerSocketPort;
- });
+ },
+ true /* enableEncrypt */);
}
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -1814,17 +1842,22 @@
ipsecNetwork.bindSocket(outSocket.mSocket);
int innerSocketPort = outSocket.getPort();
- int expectedPacketSize =
- getPacketSize(
- AF_INET6,
- AF_INET6,
- false /* useEncap */,
- false /* transportInTunnelMode */);
+ int outSeqNum = 1;
+ int receivedTestDataEspCnt = 0;
- for (int i = 0; i < PACKET_COUNT; i++) {
+ while (receivedTestDataEspCnt < PACKET_COUNT) {
outSocket.sendTo(TEST_DATA, REMOTE_INNER_6, innerSocketPort);
- tunUtils.awaitEspPacketNoPlaintext(
- spi, TEST_DATA, false /* useEncap */, expectedPacketSize);
+
+ byte[] pkt = null;
+
+ // If it is an ESP that contains the TEST_DATA, move to the next
+ // loop. Otherwise, the ESP may contain an ICMPv6(Router Solicitation).
+ // In this case, just increase the expected sequence number and continue
+ // waiting for the ESP with TEST_DATA
+ do {
+ pkt = tunUtils.awaitEspPacket(spi, false /* useEncap */, outSeqNum++);
+ } while (CollectionUtils.indexOfSubArray(pkt, TEST_DATA) == -1);
+ receivedTestDataEspCnt++;
}
final int innerPacketSize =
@@ -1838,6 +1871,7 @@
newReplayBitmap(0));
return innerSocketPort;
- });
+ },
+ false /* enableEncrypt */);
}
}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 06a827b..2c7d5c6 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -40,10 +40,10 @@
import android.system.OsConstants;
import android.util.ArraySet;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.DeviceConfigRule;
import org.junit.Before;
@@ -53,7 +53,8 @@
import java.util.Set;
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+@RunWith(DevSdkIgnoreRunner.class)
public class MultinetworkApiTest {
@Rule(order = 1)
public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index beb9274..60081d4 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -194,15 +194,16 @@
// TODO : enable this in a Mainline update or in V.
private const val SHOULD_CREATE_NETWORKS_IMMEDIATELY = false
-@RunWith(DevSdkIgnoreRunner::class)
-// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
-// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
-@IgnoreUpTo(Build.VERSION_CODES.R)
+@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
// NetworkAgent is updated as part of the connectivity module, and running NetworkAgent tests in MTS
// for modules other than Connectivity does not provide much value. Only run them in connectivity
// module MTS, so the tests only need to cover the case of an updated NetworkAgent.
@ConnectivityModuleTest
-@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
+// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
class NetworkAgentTest {
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
index 621af23..f9acb66 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -19,7 +19,6 @@
import android.Manifest.permission.MANAGE_TEST_NETWORKS
import android.Manifest.permission.NETWORK_SETTINGS
import android.content.Context
-import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.EthernetManager
import android.net.InetAddresses
@@ -36,7 +35,6 @@
import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
import android.net.dhcp.DhcpRequestPacket
-import android.os.Build
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import androidx.test.platform.app.InstrumentationRegistry
@@ -44,7 +42,7 @@
import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.AutoCloseTestInterfaceRule
import com.android.testutils.DhcpClientPacketFilter
import com.android.testutils.DhcpOptionFilter
import com.android.testutils.RecorderCallback.CallbackEntry
@@ -79,10 +77,6 @@
@AppModeFull(reason = "Instant apps cannot create test networks")
@RunWith(AndroidJUnit4::class)
class NetworkValidationTest {
- @JvmField
- @Rule
- val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
@@ -104,23 +98,24 @@
private var testSkipped = false
+ @get:Rule
+ val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
@Before
fun setUp() {
// This test requires using a tap interface as an ethernet interface.
- val pm = context.getPackageManager()
- testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
- context.getSystemService(EthernetManager::class.java) == null
+ testSkipped = context.getSystemService(EthernetManager::class.java) == null
assumeFalse(testSkipped)
// Register a request so the network does not get torn down
cm.requestNetwork(ethRequest, ethRequestCb)
runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
eth.setIncludeTestInterfaces(true)
- // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
- // does not go out of scope, which would cause it to close the underlying FileDescriptor
- // in its finalizer.
- iface = tnm.createTapInterface()
}
+ // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+ // does not go out of scope, which would cause it to close the underlying FileDescriptor
+ // in its finalizer.
+ iface = testInterfaceRule.createTapInterface()
handlerThread.start()
reader = TapPacketReader(
@@ -147,8 +142,6 @@
handlerThread.threadHandler.post { reader.stop() }
handlerThread.quitSafely()
handlerThread.join()
-
- iface.fileDescriptor.close()
}
@Test
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index 1b1f367..24af42b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -17,18 +17,22 @@
import android.net.EthernetTetheringTestBase
import android.net.LinkAddress
-import android.net.TestNetworkInterface
import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
import android.net.TetheringManager.TETHERING_ETHERNET
import android.net.TetheringManager.TetheringRequest
+import android.net.cts.util.EthernetTestInterface
import android.net.nsd.NsdManager
import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import androidx.test.filters.SmallTest
+import com.android.testutils.AutoCloseTestInterfaceRule
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.TapPacketReader
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.pollForQuery
import com.android.testutils.tryTest
import java.util.Random
import kotlin.test.assertEquals
@@ -36,9 +40,12 @@
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+private const val TAG = "NsdManagerDownstreamTetheringTest"
+
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@ConnectivityModuleTest
@@ -48,32 +55,41 @@
private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+ private val handlerThread = HandlerThread("$TAG thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private lateinit var downstreamIface: EthernetTestInterface
+ private var tetheringEventCallback: MyTetheringEventCallback? = null
+
+ @get:Rule
+ val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
@Before
override fun setUp() {
super.setUp()
- setIncludeTestInterfaces(true)
+ val iface = testInterfaceRule.createTapInterface()
+ downstreamIface = EthernetTestInterface(context, handler, iface)
}
@After
override fun tearDown() {
+ if (::downstreamIface.isInitialized) {
+ downstreamIface.destroy()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ maybeUnregisterTetheringEventCallback(tetheringEventCallback)
super.tearDown()
- setIncludeTestInterfaces(false)
}
@Test
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
assumeFalse(isInterfaceForTetheringAvailable())
- var downstreamIface: TestNetworkInterface? = null
- var tetheringEventCallback: MyTetheringEventCallback? = null
- var downstreamReader: TapPacketReader? = null
-
val discoveryRecord = NsdDiscoveryRecord()
tryTest {
- downstreamIface = createTestInterface()
- val iface = tetheredInterface
- assertEquals(iface, downstreamIface?.interfaceName)
+ val iface = mTetheredInterfaceRequester.getInterface()
+ assertEquals(downstreamIface.name, iface)
val request = TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
tetheringEventCallback = enableEthernetTethering(
@@ -82,41 +98,27 @@
).apply {
awaitInterfaceLocalOnly()
}
- // This shouldn't be flaky because the TAP interface will buffer all packets even
- // before the reader is started.
- downstreamReader = makePacketReader(downstreamIface)
+ val downstreamReader = downstreamIface.packetReader
waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
- assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+ assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
- } cleanupStep {
- maybeStopTapPacketReader(downstreamReader)
- } cleanupStep {
- maybeCloseTestInterface(downstreamIface)
- } cleanup {
- maybeUnregisterTetheringEventCallback(tetheringEventCallback)
}
}
@Test
fun testMdnsDiscoveryWorkOnTetheringInterface() {
assumeFalse(isInterfaceForTetheringAvailable())
- setIncludeTestInterfaces(true)
-
- var downstreamIface: TestNetworkInterface? = null
- var tetheringEventCallback: MyTetheringEventCallback? = null
- var downstreamReader: TapPacketReader? = null
val discoveryRecord = NsdDiscoveryRecord()
tryTest {
- downstreamIface = createTestInterface()
- val iface = tetheredInterface
- assertEquals(iface, downstreamIface?.interfaceName)
+ val iface = mTetheredInterfaceRequester.getInterface()
+ assertEquals(downstreamIface.name, iface)
val localAddr = LinkAddress("192.0.2.3/28")
val clientAddr = LinkAddress("192.0.2.2/28")
@@ -130,23 +132,14 @@
awaitInterfaceTethered()
}
- val fd = downstreamIface?.fileDescriptor?.fileDescriptor
- assertNotNull(fd)
- downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
-
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
- assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+ val downstreamReader = downstreamIface.packetReader
+ assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
// TODO: Add another test to check packet reply can trigger serviceFound.
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
- } cleanupStep {
- maybeStopTapPacketReader(downstreamReader)
- } cleanupStep {
- maybeCloseTestInterface(downstreamIface)
- } cleanup {
- maybeUnregisterTetheringEventCallback(tetheringEventCallback)
}
}
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 6394599..c71d925 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -39,19 +39,6 @@
import android.net.TestNetworkManager
import android.net.TestNetworkSpecifier
import android.net.connectivity.ConnectivityCompatChanges
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
-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
-import android.net.cts.NsdResolveRecord.ResolveEvent.ServiceResolved
-import android.net.cts.NsdResolveRecord.ResolveEvent.StopResolutionFailed
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
-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
@@ -92,16 +79,40 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DeviceConfigRule
import com.android.testutils.NSResponder
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import com.android.testutils.NsdEvent
+import com.android.testutils.NsdRecord
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ResolutionStopped
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.android.testutils.NsdResolveRecord.ResolveEvent.StopResolutionFailed
+import com.android.testutils.NsdServiceInfoCallbackRecord
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TapPacketReader
+import com.android.testutils.TestDnsPacket
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.assertContainsExactly
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
+import com.android.testutils.pollForAdvertisement
+import com.android.testutils.pollForMdnsPacket
+import com.android.testutils.pollForProbe
+import com.android.testutils.pollForQuery
+import com.android.testutils.pollForReply
import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import com.android.testutils.waitForIdle
@@ -116,8 +127,10 @@
import java.util.Random
import java.util.concurrent.Executor
import kotlin.math.min
+import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
@@ -130,10 +143,10 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertNotEquals
private const val TAG = "NsdManagerTest"
private const val TIMEOUT_MS = 2000L
+
// Registration may take a long time if there are devices with the same hostname on the network,
// as the device needs to try another name and probe again. This is especially true since when using
// mdnsresponder the usual hostname is "Android", and on conflict "Android-2", "Android-3", ... are
@@ -175,11 +188,12 @@
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 publicKey = hexStringToByteArray(
- "0201030dc141d0637960b98cbc12cfca"
- + "221d2879dac26ee5b460e9007c992e19"
- + "02d897c391b03764d448f7d0c772fdb0"
- + "3b1d9d6d52ff8886769e8e2362513565"
- + "270962d3")
+ "0201030dc141d0637960b98cbc12cfca" +
+ "221d2879dac26ee5b460e9007c992e19" +
+ "02d897c391b03764d448f7d0c772fdb0" +
+ "3b1d9d6d52ff8886769e8e2362513565" +
+ "270962d3"
+ )
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@@ -231,11 +245,14 @@
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
- cm.requestNetwork(NetworkRequest.Builder()
+ cm.requestNetwork(
+ NetworkRequest.Builder()
.removeCapability(NET_CAPABILITY_TRUSTED)
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(testNetworkSpecifier)
- .build(), cb)
+ .build(),
+ cb
+ )
val agent = registerTestNetworkAgent(iface.interfaceName)
val network = agent.network ?: fail("Registered agent should have a network")
@@ -255,12 +272,17 @@
val lp = LinkProperties().apply {
interfaceName = ifaceName
}
- val agent = TestableNetworkAgent(context, handlerThread.looper,
+ val agent = TestableNetworkAgent(
+ context,
+ handlerThread.looper,
NetworkCapabilities().apply {
removeCapability(NET_CAPABILITY_TRUSTED)
addTransportType(TRANSPORT_TEST)
setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- }, lp, NetworkAgentConfig.Builder().build())
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
val network = agent.register()
agent.markConnected()
agent.expectCallback<OnNetworkCreated>()
@@ -334,15 +356,19 @@
Triple(null, null, "null key"),
Triple("", null, "empty key"),
Triple(string256, null, "key with 256 characters"),
- Triple("key", string256.substring(3),
- "key+value combination with more than 255 characters"),
+ Triple(
+ "key",
+ string256.substring(3),
+ "key+value combination with more than 255 characters"
+ ),
Triple("key", string256.substring(4), "key+value combination with 255 characters"),
Triple("\u0019", null, "key with invalid character"),
Triple("=", null, "key with invalid character"),
Triple("\u007f", null, "key with invalid character")
).forEach {
assertFailsWith<IllegalArgumentException>(
- "Setting invalid ${it.third} unexpectedly succeeded") {
+ "Setting invalid ${it.third} unexpectedly succeeded"
+ ) {
si.setAttribute(it.first, it.second)
}
}
@@ -365,7 +391,8 @@
// Test registering without an Executor
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
- REGISTRATION_TIMEOUT_MS).serviceInfo
+ REGISTRATION_TIMEOUT_MS
+ ).serviceInfo
// Only service name is included in ServiceRegistered callbacks
assertNull(registeredInfo.serviceType)
@@ -380,7 +407,9 @@
// Expect a service record to be discovered
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- registeredInfo.serviceName, serviceType)
+ registeredInfo.serviceName,
+ serviceType
+ )
// Test resolving without an Executor
val resolveRecord = NsdResolveRecord()
@@ -395,8 +424,10 @@
assertNull(resolvedService.attributes["booleanAttr"])
assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
- assertEquals(" value ",
- resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+ assertEquals(
+ " value ",
+ resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString()
+ )
assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
@@ -430,12 +461,15 @@
val registrationRecord2 = NsdRegistrationRecord()
nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>(
- REGISTRATION_TIMEOUT_MS).serviceInfo
+ REGISTRATION_TIMEOUT_MS
+ ).serviceInfo
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
- registeredInfo2.serviceName, serviceType)
+ registeredInfo2.serviceName,
+ serviceType
+ )
// Resolve the service
val resolveRecord2 = NsdResolveRecord()
@@ -938,7 +972,8 @@
// when the compat change is disabled.
// Note that before T the compat constant had a different int value.
assertFalse(CompatChanges.isChangeEnabled(
- ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
+ ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER
+ ))
}
@Test
@@ -1011,7 +1046,9 @@
// onStopResolutionFailed on the record directly then verify it is received.
val resolveRecord = NsdResolveRecord()
resolveRecord.onStopResolutionFailed(
- NsdServiceInfo(), NsdManager.FAILURE_OPERATION_NOT_RUNNING)
+ NsdServiceInfo(),
+ NsdManager.FAILURE_OPERATION_NOT_RUNNING
+ )
val failedCb = resolveRecord.expectCallback<StopResolutionFailed>()
assertEquals(NsdManager.FAILURE_OPERATION_NOT_RUNNING, failedCb.errorCode)
}
@@ -1262,15 +1299,22 @@
val si = makeTestServiceInfo(testNetwork1.network)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ 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)
+ nsdManager.registerService(
+ si,
+ NsdManager.PROTOCOL_DNS_SD,
+ { it.run() },
+ registrationRecord
+ )
tryTest {
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1301,15 +1345,22 @@
parseNumericAddress("2001:db8::3"))
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ 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)
+ nsdManager.registerService(
+ si,
+ NsdManager.PROTOCOL_DNS_SD,
+ { it.run() },
+ registrationRecord
+ )
tryTest {
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1340,15 +1391,22 @@
hostname = customHostname
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ 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)
+ nsdManager.registerService(
+ si,
+ NsdManager.PROTOCOL_DNS_SD,
+ { it.run() },
+ registrationRecord
+ )
tryTest {
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1380,8 +1438,11 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1459,7 +1520,9 @@
val registeredService = registerService(registrationRecord, si)
val packetReader = TapPacketReader(
Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1489,11 +1552,11 @@
val newRegistration =
registrationRecord
.expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
- it.serviceInfo.serviceName == serviceName
- && it.serviceInfo.hostname.let { hostname ->
- hostname != null
- && hostname.startsWith(customHostname)
- && hostname != customHostname
+ it.serviceInfo.serviceName == serviceName &&
+ it.serviceInfo.hostname.let { hostname ->
+ hostname != null &&
+ hostname.startsWith(customHostname) &&
+ hostname != customHostname
}
}
@@ -1524,8 +1587,11 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1564,13 +1630,21 @@
fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
// Register service on testNetwork1
val discoveryRecord = NsdDiscoveryRecord()
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord)
+ nsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord
+ )
tryTest {
discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1614,8 +1688,10 @@
fun testResolveWhenServerSendsNoAdditionalRecord() {
// Resolve service on testNetwork1
val resolveRecord = NsdResolveRecord()
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
)
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1639,16 +1715,21 @@
rdata='testkey=testvalue')
))).hex()
*/
- val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
+ val srvTxtResponsePayload = HexDump.hexStringToByteArray(
+ "000084000000000200000000104" +
"e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
"3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
- "00078001211746573746b65793d7465737476616c7565")
+ "00078001211746573746b65793d7465737476616c7565"
+ )
replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
val testHostname = "testhost.local"
- val addressQuery = packetReader.pollForQuery(testHostname,
- DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
+ val addressQuery = packetReader.pollForQuery(
+ testHostname,
+ DnsResolver.TYPE_A,
+ DnsResolver.TYPE_AAAA
+ )
assertNotNull(addressQuery)
/*
@@ -1660,9 +1741,11 @@
rdata='2001:db8::123')
))).hex()
*/
- val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
+ val addressPayload = HexDump.hexStringToByteArray(
+ "0000840000000002000000000874657374" +
"686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
- "010db8000000000000000000000123")
+ "010db8000000000000000000000123"
+ )
packetReader.sendResponse(buildMdnsPacket(addressPayload))
val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
@@ -1676,7 +1759,8 @@
}
assertEquals(
setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
- serviceResolved.serviceInfo.hostAddresses.toSet())
+ serviceResolved.serviceInfo.hostAddresses.toSet()
+ )
}
@Test
@@ -1703,7 +1787,7 @@
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)
@@ -1759,7 +1843,7 @@
an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
rdata='NsdTest123456789._nmt123456789._tcp.local')
)).hex()
- */
+ */
val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
"536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
"d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
@@ -1794,7 +1878,7 @@
an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
rdata='NsdTest123456789._nmt123456789._tcp.local')
)).hex()
- */
+ */
val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
"3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
"0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
@@ -1846,7 +1930,7 @@
scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
qclass=0x8001)
)).hex()
- */
+ */
val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
"536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
"d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
@@ -1858,7 +1942,7 @@
an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
rdata='NsdTest._test._tcp.local')
)).hex()
- */
+ */
val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
"045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
"746370056c6f63616c00")
@@ -1870,7 +1954,7 @@
an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
rdata='NsdTest123456789._nmt123456789._tcp.local')
)).hex()
- */
+ */
val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
"3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
"3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
@@ -1907,13 +1991,21 @@
// Register service on testNetwork1
val discoveryRecord = NsdDiscoveryRecord()
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord)
+ nsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord
+ )
tryTest {
discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1975,10 +2067,12 @@
val hostAddresses1 = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
- parseNumericAddress("2001:db8::2"))
+ parseNumericAddress("2001:db8::2")
+ )
val hostAddresses2 = listOf(
parseNumericAddress("192.0.2.24"),
- parseNumericAddress("2001:db8::3"))
+ parseNumericAddress("2001:db8::3")
+ )
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceName = serviceName
@@ -2042,10 +2136,12 @@
val hostAddresses1 = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
- parseNumericAddress("2001:db8::2"))
+ parseNumericAddress("2001:db8::2")
+ )
val hostAddresses2 = listOf(
parseNumericAddress("192.0.2.24"),
- parseNumericAddress("2001:db8::3"))
+ parseNumericAddress("2001:db8::3")
+ )
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.hostname = customHostname
@@ -2093,7 +2189,8 @@
val hostAddresses = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
- parseNumericAddress("2001:db8::2"))
+ parseNumericAddress("2001:db8::2")
+ )
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceType = serviceType
@@ -2258,8 +2355,11 @@
it.port = TEST_PORT
it.publicKey = publicKey
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -2310,8 +2410,11 @@
parseNumericAddress("2001:db8::2"))
it.publicKey = publicKey
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -2364,8 +2467,11 @@
it.hostAddresses = listOf()
it.publicKey = publicKey
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -2418,10 +2524,20 @@
val discoveryRecord1 = NsdDiscoveryRecord()
val discoveryRecord2 = NsdDiscoveryRecord()
val discoveryRecord3 = NsdDiscoveryRecord()
- nsdManager.discoverServices("_test1._tcp", NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord1)
- nsdManager.discoverServices("_test2._tcp", NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord2)
+ nsdManager.discoverServices(
+ "_test1._tcp",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord1
+ )
+ nsdManager.discoverServices(
+ "_test2._tcp",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord2
+ )
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord3)
tryTest {
@@ -2459,6 +2575,44 @@
}
}
+ @Test
+ fun testAdvertiseServiceWithNoAttributes_TxtRecordIstNotEmpty() {
+ deviceConfigRule.setConfig(
+ NAMESPACE_TETHERING,
+ "test_nsd_avoid_advertising_empty_txt_records",
+ "1"
+ )
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ )
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ // Test behavior described in RFC6763 6.1: empty TXT records are not allowed, but TXT
+ // records with a zero length string are equivalent.
+ val si = makeTestServiceInfo(testNetwork1.network)
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ registerService(registrationRecord, si)
+
+ tryTest {
+ val announcement =
+ packetReader.pollForReply("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT)
+ assertNotNull(announcement)
+ val txtRecords = announcement.records[ANSECTION].filter {
+ it.nsType == DnsResolver.TYPE_TXT
+ }
+ assertEquals(1, txtRecords.size)
+ // The TXT record should contain as single zero
+ assertContentEquals(byteArrayOf(0), txtRecords[0].rr)
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
return clients.any { client -> client.substring(
client.indexOf("network=") + "network=".length,
@@ -2477,11 +2631,16 @@
*/
private fun getServiceTypeClients(): List<String> {
return SystemUtil.runShellCommand(
- InstrumentationRegistry.getInstrumentation(), "dumpsys servicediscovery")
+ InstrumentationRegistry.getInstrumentation(),
+ "dumpsys servicediscovery"
+ )
.split("\n").mapNotNull { line ->
line.indexOf("ServiceTypeClient:").let { idx ->
- if (idx == -1) null
- else line.substring(idx)
+ if (idx == -1) {
+ null
+ } else {
+ line.substring(idx)
+ }
}
}
}
@@ -2494,9 +2653,11 @@
rclass=0x8001, port=31234, target='conflict.local', ttl=120)
)).hex()
*/
- val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
+ val mdnsPayload = HexDump.hexStringToByteArray(
+ "000084000000000100000000104e736454657" +
"3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
- "18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
+ "18001000000780016000000007a0208636f6e666c696374056c6f63616c00"
+ )
replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
return buildMdnsPacket(mdnsPayload)
@@ -2510,9 +2671,11 @@
rdata='2001:db8::321')
)).hex()
*/
- val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+ val mdnsPayload = HexDump.hexStringToByteArray(
+ "000084000000000100000000144e7364" +
"54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
- "00000000000000000321")
+ "00000000000000000321"
+ )
replaceCustomHostnameWithTestSuffix(mdnsPayload)
return buildMdnsPacket(mdnsPayload)
@@ -2563,22 +2726,29 @@
mdnsPayload: ByteArray,
srcAddr: Inet6Address = testSrcAddr
): ByteBuffer {
- val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
- IPPROTO_UDP, mdnsPayload.size)
+ val packetBuffer = PacketBuilder.allocate(
+ true /* hasEther */,
+ IPPROTO_IPV6,
+ IPPROTO_UDP,
+ mdnsPayload.size
+ )
val packetBuilder = PacketBuilder(packetBuffer)
// Multicast ethernet address for IPv6 to ff02::fb
val multicastEthAddr = MacAddress.fromBytes(
- byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte()))
+ byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte())
+ )
packetBuilder.writeL2Header(
MacAddress.fromBytes(byteArrayOf(1, 2, 3, 4, 5, 6)) /* srcMac */,
multicastEthAddr,
- ETH_P_IPV6.toShort())
+ ETH_P_IPV6.toShort()
+ )
packetBuilder.writeIpv6Header(
0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
IPPROTO_UDP.toByte(),
64 /* hop limit */,
srcAddr,
- multicastIpv6Addr /* dstIp */)
+ multicastIpv6Addr /* dstIp */
+ )
packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
packetBuffer.put(mdnsPayload)
return packetBuilder.finalizePacket()
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
index 4d924d1..1588835 100644
--- a/tests/cts/net/src/android/net/cts/PacketUtils.java
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -49,6 +49,7 @@
static final int TCP_HDRLEN = 20;
static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
static final int ESP_HDRLEN = 8;
+ static final int ICMP_HDRLEN = 8;
static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned
static final int ESP_TRAILER_LEN = 2;
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
index 268d8d2..8bf4998 100644
--- a/tests/cts/net/src/android/net/cts/TunUtils.java
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -47,6 +47,8 @@
protected static final int IP4_PROTO_OFFSET = 9;
protected static final int IP6_PROTO_OFFSET = 6;
+ private static final int SEQ_NUM_MATCH_NOT_REQUIRED = -1;
+
private static final int DATA_BUFFER_LEN = 4096;
private static final int TIMEOUT = 2000;
@@ -146,16 +148,30 @@
return espPkt; // We've found the packet we're looking for.
}
+ /** Await the expected ESP packet */
public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception {
- return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap));
+ return awaitEspPacket(spi, useEncap, SEQ_NUM_MATCH_NOT_REQUIRED);
}
- private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+ /** Await the expected ESP packet with a matching sequence number */
+ public byte[] awaitEspPacket(int spi, boolean useEncap, int seqNum) throws Exception {
+ return awaitPacket((pkt) -> isEsp(pkt, spi, seqNum, useEncap));
+ }
+
+ private static boolean isMatchingEspPacket(byte[] pkt, int espOffset, int spi, int seqNum) {
ByteBuffer buffer = ByteBuffer.wrap(pkt);
buffer.get(new byte[espOffset]); // Skip IP, UDP header
int actualSpi = buffer.getInt();
+ int actualSeqNum = buffer.getInt();
- return actualSpi == spi;
+ if (actualSeqNum < 0) {
+ throw new UnsupportedOperationException(
+ "actualSeqNum overflowed and needs to be converted to an unsigned integer");
+ }
+
+ boolean isSeqNumMatched = (seqNum == SEQ_NUM_MATCH_NOT_REQUIRED || seqNum == actualSeqNum);
+
+ return actualSpi == spi && isSeqNumMatched;
}
/**
@@ -173,29 +189,32 @@
fail("Banned plaintext packet found");
}
- return isEsp(pkt, spi, encap);
+ return isEsp(pkt, spi, SEQ_NUM_MATCH_NOT_REQUIRED, encap);
}
- private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+ private static boolean isEsp(byte[] pkt, int spi, int seqNum, boolean encap) {
if (isIpv6(pkt)) {
if (encap) {
return pkt[IP6_PROTO_OFFSET] == IPPROTO_UDP
- && isSpiEqual(pkt, IP6_HDRLEN + UDP_HDRLEN, spi);
+ && isMatchingEspPacket(pkt, IP6_HDRLEN + UDP_HDRLEN, spi, seqNum);
} else {
- return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+ return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP
+ && isMatchingEspPacket(pkt, IP6_HDRLEN, spi, seqNum);
}
} else {
// Use default IPv4 header length (assuming no options)
if (encap) {
return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP
- && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi);
+ && isMatchingEspPacket(pkt, IP4_HDRLEN + UDP_HDRLEN, spi, seqNum);
} else {
- return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+ return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP
+ && isMatchingEspPacket(pkt, IP4_HDRLEN, spi, seqNum);
}
}
}
+
public static boolean isIpv6(byte[] pkt) {
// First nibble shows IP version. 0x60 for IPv6
return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
diff --git a/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
new file mode 100644
index 0000000..32d6899
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.util
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.IpConfiguration
+import android.net.TestNetworkInterface
+import android.net.cts.util.EthernetTestInterface.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TapPacketReader
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import java.net.NetworkInterface
+import kotlin.concurrent.Volatile
+import kotlin.test.assertNotNull
+
+private const val TAG = "EthernetTestInterface"
+private const val TIMEOUT_MS = 5_000L
+
+/**
+ * A class to manage the lifecycle of an ethernet interface.
+ *
+ * This class encapsulates creating new tun/tap interfaces and registering them with ethernet
+ * service.
+ */
+class EthernetTestInterface(
+ private val context: Context,
+ private val handler: Handler,
+ val testIface: TestNetworkInterface
+) {
+ private class EthernetStateListener(private val trackedIface: String) : InterfaceStateListener {
+ val events = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+ sealed class CallbackEntry {
+ data class InterfaceStateChanged(
+ val iface: String,
+ val state: Int,
+ val role: Int,
+ val cfg: IpConfiguration?
+ ) : CallbackEntry()
+ }
+
+ override fun onInterfaceStateChanged(
+ iface: String,
+ state: Int,
+ role: Int,
+ cfg: IpConfiguration?
+ ) {
+ // filter out callbacks for other interfaces
+ if (iface != trackedIface) return
+ events.add(InterfaceStateChanged(iface, state, role, cfg))
+ }
+
+ fun eventuallyExpect(state: Int) {
+ val cb = events.poll(TIMEOUT_MS) { it is InterfaceStateChanged && it.state == state }
+ assertNotNull(cb, "Never received state $state. Got: ${events.backtrace()}")
+ }
+ }
+
+ val name get() = testIface.interfaceName
+ val mtu: Int
+ get() {
+ val nif = NetworkInterface.getByName(name)
+ assertNotNull(nif)
+ return nif.mtu
+ }
+ val packetReader = TapPacketReader(handler, testIface.fileDescriptor.fileDescriptor, mtu)
+ private val listener = EthernetStateListener(name)
+ private val em = context.getSystemService(EthernetManager::class.java)!!
+ @Volatile private var cleanedUp = false
+
+ init{
+ em.addInterfaceStateListener(handler::post, listener)
+ runAsShell(NETWORK_SETTINGS) {
+ em.setIncludeTestInterfaces(true)
+ }
+ // Wait for link up to be processed in EthernetManager before returning.
+ listener.eventuallyExpect(STATE_LINK_UP)
+ handler.post { packetReader.start() }
+ handler.waitForIdle(TIMEOUT_MS)
+ }
+
+ fun destroy() {
+ // packetReader.stop() closes the test interface.
+ handler.post { packetReader.stop() }
+ handler.waitForIdle(TIMEOUT_MS)
+ listener.eventuallyExpect(STATE_ABSENT)
+
+ // setIncludeTestInterfaces() posts on the handler and does not run synchronously. However,
+ // there should be no need for a synchronization mechanism here. If the next test is
+ // bringing up its interface, a RTM_NEWLINK will be put on the back of the handler and is
+ // guaranteed to be in order with (i.e. after) this call, so there is no race here.
+ runAsShell(NETWORK_SETTINGS) {
+ em.setIncludeTestInterfaces(false)
+ }
+ em.removeInterfaceStateListener(listener)
+
+ cleanedUp = true
+ }
+
+ protected fun finalize() {
+ if (!cleanedUp) {
+ Log.wtf(TAG, "destroy() was not called for interface $name.")
+ destroy()
+ }
+ }
+}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 1023173..1165018 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -22,7 +22,7 @@
defaults: ["cts_defaults"],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
],
srcs: [
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 81608f7..1454d9a 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -22,6 +22,9 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
@@ -34,6 +37,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -60,7 +64,9 @@
import android.net.cts.util.CtsTetheringUtils;
import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.net.wifi.WifiSsid;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
@@ -71,6 +77,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.ParcelUtils;
import org.junit.After;
@@ -78,6 +85,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -216,12 +224,26 @@
@Test
public void testTetheringRequest() {
- final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ SoftApConfiguration softApConfiguration;
+ if (SdkLevel.isAtLeastT()) {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(
+ "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setSsid("This is an SSID!")
+ .build();
+ }
+ final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfiguration)
+ .build();
assertEquals(TETHERING_WIFI, tr.getTetheringType());
assertNull(tr.getLocalIpv4Address());
assertNull(tr.getClientStaticIpv4Address());
assertFalse(tr.isExemptFromEntitlementCheck());
assertTrue(tr.getShouldShowEntitlementUi());
+ assertEquals(softApConfiguration, tr.getSoftApConfiguration());
final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
@@ -235,26 +257,66 @@
assertEquals(TETHERING_USB, tr2.getTetheringType());
assertTrue(tr2.isExemptFromEntitlementCheck());
assertFalse(tr2.getShouldShowEntitlementUi());
+
+ final TetheringRequest tr3 = new TetheringRequest.Builder(TETHERING_USB)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+ assertEquals(tr2, tr3);
+ }
+
+ @Test
+ public void testTetheringRequestSetSoftApConfigurationFailsWhenNotWifi() {
+ final SoftApConfiguration softApConfiguration;
+ if (SdkLevel.isAtLeastT()) {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(
+ "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setSsid("This is an SSID!")
+ .build();
+ }
+ for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_WIFI_P2P,
+ TETHERING_NCM, TETHERING_ETHERNET)) {
+ try {
+ new TetheringRequest.Builder(type).setSoftApConfiguration(softApConfiguration);
+ fail("Was able to set SoftApConfiguration for tethering type " + type);
+ } catch (IllegalArgumentException e) {
+ // Success
+ }
+ }
}
@Test
public void testTetheringRequestParcelable() {
+ final SoftApConfiguration softApConfiguration;
+ if (SdkLevel.isAtLeastT()) {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(
+ "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setSsid("This is an SSID!")
+ .build();
+ }
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)
+ final TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfiguration)
.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());
+ final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+ assertEquals(withConfig, ParcelUtils.parcelingRoundTrip(withConfig));
+ assertEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+ assertNotEquals(withConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+ assertNotEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withConfig));
}
@Test
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 349529dd..6c3b7a0 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -33,7 +33,7 @@
"src/**/*.aidl",
],
libs: [
- "android.test.mock",
+ "android.test.mock.stubs",
"ServiceConnectivityResources",
],
static_libs: [
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index c5088c6..39a08fa 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -21,6 +21,7 @@
stl: "libc++_static",
shared_libs: [
"libbinder_ndk",
+ "libcom.android.tethering.connectivity_native",
"liblog",
"libnetutils",
"libprocessgroup",
@@ -32,7 +33,7 @@
"libmodules-utils-build",
"libutils",
],
- compile_multilib: "first",
+ compile_multilib: "both",
}
filegroup {
diff --git a/tests/native/connectivity_native_test/AndroidTestTemplate.xml b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
index 44e35a9..4ea114c 100644
--- a/tests/native/connectivity_native_test/AndroidTestTemplate.xml
+++ b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
@@ -15,8 +15,10 @@
<configuration description="Configuration for connectivity {MODULE} tests">
<option name="test-suite-tag" value="mts" />
<option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
- <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+ <!-- The tested library is only usable on U+ -->
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+ <!-- The tested library is only available with the primary abi -->
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index f62a30b..faaf150 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -14,25 +14,17 @@
* limitations under the License.
*/
+#include <android-modules-utils/sdk_level.h>
#include <android/binder_manager.h>
#include <android/binder_process.h>
-#include <android-modules-utils/sdk_level.h>
-#include <cutils/misc.h> // FIRST_APPLICATION_UID
+#include <connectivity_native.h>
+#include <cutils/misc.h> // FIRST_APPLICATION_UID
#include <dlfcn.h>
#include <gtest/gtest.h>
#include <netinet/in.h>
#include "bpf/BpfUtils.h"
-typedef int (*GetPortsBlockedForBind)(in_port_t*, size_t*);
-GetPortsBlockedForBind getPortsBlockedForBind;
-typedef int (*BlockPortForBind)(in_port_t);
-BlockPortForBind blockPortForBind;
-typedef int (*UnblockPortForBind)(in_port_t);
-UnblockPortForBind unblockPortForBind;
-typedef int (*UnblockAllPortsForBind)();
-UnblockAllPortsForBind unblockAllPortsForBind;
-
class ConnectivityNativeBinderTest : public ::testing::Test {
public:
in_port_t mActualBlockedPorts[65535];
@@ -50,29 +42,12 @@
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.
- // TODO: link against the library directly and add Sdk34ModuleController to
- // AndroidTest.txml when available.
- void* nativeLib = dlopen("libcom.android.tethering.connectivity_native.so", RTLD_NOW);
- ASSERT_NE(nullptr, nativeLib);
- getPortsBlockedForBind = reinterpret_cast<GetPortsBlockedForBind>(
- dlsym(nativeLib, "AConnectivityNative_getPortsBlockedForBind"));
- ASSERT_NE(nullptr, getPortsBlockedForBind);
- blockPortForBind = reinterpret_cast<BlockPortForBind>(
- dlsym(nativeLib, "AConnectivityNative_blockPortForBind"));
- ASSERT_NE(nullptr, blockPortForBind);
- unblockPortForBind = reinterpret_cast<UnblockPortForBind>(
- dlsym(nativeLib, "AConnectivityNative_unblockPortForBind"));
- ASSERT_NE(nullptr, unblockPortForBind);
- unblockAllPortsForBind = reinterpret_cast<UnblockAllPortsForBind>(
- dlsym(nativeLib, "AConnectivityNative_unblockAllPortsForBind"));
- ASSERT_NE(nullptr, unblockAllPortsForBind);
-
- // If there are already ports being blocked on device unblockAllPortsForBind() store
- // the currently blocked ports and add them back at the end of the test. Do this for
- // every test case so additional test cases do not forget to add ports back.
- int err = getPortsBlockedForBind(mActualBlockedPorts, &mActualBlockedPortsCount);
+ // If there are already ports being blocked on device store the
+ // currently blocked ports and add them back at the end of the test. Do
+ // this for every test case so additional test cases do not forget to
+ // add ports back.
+ int err = AConnectivityNative_getPortsBlockedForBind(
+ mActualBlockedPorts, &mActualBlockedPortsCount);
EXPECT_EQ(err, 0);
restoreBlockedPorts = true;
}
@@ -81,8 +56,9 @@
int err;
if (mActualBlockedPortsCount > 0 && restoreBlockedPorts) {
for (int i=0; i < mActualBlockedPortsCount; i++) {
- err = blockPortForBind(mActualBlockedPorts[i]);
- EXPECT_EQ(err, 0);
+ err =
+ AConnectivityNative_blockPortForBind(mActualBlockedPorts[i]);
+ EXPECT_EQ(err, 0);
}
}
}
@@ -99,7 +75,7 @@
int blockedPort = 0;
if (blockPort) {
blockedPort = ntohs(port);
- err = blockPortForBind(blockedPort);
+ err = AConnectivityNative_blockPortForBind(blockedPort);
EXPECT_EQ(err, 0);
}
@@ -107,7 +83,7 @@
if (blockPort) {
EXPECT_EQ(-1, sock3);
- err = unblockPortForBind(blockedPort);
+ err = AConnectivityNative_unblockPortForBind(blockedPort);
EXPECT_EQ(err, 0);
} else {
EXPECT_NE(-1, sock3);
@@ -197,11 +173,11 @@
}
TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
- int err = blockPortForBind(5555);
+ int err = AConnectivityNative_blockPortForBind(5555);
EXPECT_EQ(err, 0);
- err = blockPortForBind(5555);
+ err = AConnectivityNative_blockPortForBind(5555);
EXPECT_EQ(err, 0);
- err = unblockPortForBind(5555);
+ err = AConnectivityNative_unblockPortForBind(5555);
EXPECT_EQ(err, 0);
}
@@ -210,16 +186,17 @@
in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
if (mActualBlockedPortsCount > 0) {
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
}
for (int i : blockedPorts) {
- err = blockPortForBind(i);
+ err = AConnectivityNative_blockPortForBind(i);
EXPECT_EQ(err, 0);
}
size_t actualBlockedPortsCount = 8;
in_port_t actualBlockedPorts[actualBlockedPortsCount];
- err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(
+ (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_NE(actualBlockedPortsCount, 0);
for (int i=0; i < actualBlockedPortsCount; i++) {
@@ -227,9 +204,10 @@
}
// Remove the ports we added.
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
EXPECT_EQ(err, 0);
- err = getPortsBlockedForBind(actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(actualBlockedPorts,
+ &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_EQ(actualBlockedPortsCount, 0);
}
@@ -239,23 +217,25 @@
in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
if (mActualBlockedPortsCount > 0) {
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
}
for (int i : blockedPorts) {
- err = blockPortForBind(i);
+ err = AConnectivityNative_blockPortForBind(i);
EXPECT_EQ(err, 0);
}
size_t actualBlockedPortsCount = 8;
in_port_t actualBlockedPorts[actualBlockedPortsCount];
- err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(
+ (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_EQ(actualBlockedPortsCount, 8);
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
EXPECT_EQ(err, 0);
- err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(
+ (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_EQ(actualBlockedPortsCount, 0);
// If mActualBlockedPorts is not empty, ports will be added back in teardown.
@@ -264,7 +244,7 @@
TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
int curUid = getuid();
EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
- int err = blockPortForBind((in_port_t) 5555);
+ int err = AConnectivityNative_blockPortForBind((in_port_t)5555);
EXPECT_EQ(EPERM, err);
EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 2f88c41..00f9d05 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -92,6 +92,7 @@
"frameworks-net-integration-testutils",
"framework-protos",
"mockito-target-minus-junit4",
+ "modules-utils-build",
"net-tests-utils",
"net-utils-services-common",
"platform-compat-test-rules",
@@ -103,9 +104,9 @@
],
libs: [
"android.net.ipsec.ike.stubs.module_lib",
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
"ServiceConnectivityResources",
],
exclude_kotlinc_generated_files: false,
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b71a46f..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -52,6 +52,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -72,6 +73,7 @@
import android.os.Messenger;
import android.os.Process;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.internal.util.test.BroadcastInterceptingContext;
@@ -83,6 +85,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -240,7 +243,7 @@
// register callback
when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
- anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(request);
+ anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(request);
manager.requestNetwork(request, callback, handler);
// callback triggers
@@ -269,7 +272,7 @@
// register callback
when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
- anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req1);
+ anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(req1);
manager.requestNetwork(req1, callback, handler);
// callback triggers
@@ -287,7 +290,7 @@
// callback can be registered again
when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
- anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req2);
+ anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(req2);
manager.requestNetwork(req2, callback, handler);
// callback triggers
@@ -311,7 +314,7 @@
when(mCtx.getApplicationInfo()).thenReturn(info);
when(mService.requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(),
- anyInt(), any(), nullable(String.class))).thenReturn(request);
+ anyInt(), any(), nullable(String.class), anyInt())).thenReturn(request);
Handler handler = new Handler(Looper.getMainLooper());
manager.requestNetwork(request, callback, handler);
@@ -403,15 +406,15 @@
manager.requestNetwork(request, callback);
verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
eq(REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
- eq(testPkgName), eq(testAttributionTag));
+ eq(testPkgName), eq(testAttributionTag), anyInt());
reset(mService);
// Verify that register network callback does not calls requestNetwork at all.
manager.registerNetworkCallback(request, callback);
verify(mService, never()).requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(),
- anyInt(), anyInt(), any(), any());
+ anyInt(), anyInt(), any(), any(), anyInt());
verify(mService).listenForNetwork(eq(request.networkCapabilities), any(), any(), anyInt(),
- eq(testPkgName), eq(testAttributionTag));
+ eq(testPkgName), eq(testAttributionTag), anyInt());
reset(mService);
Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
@@ -419,24 +422,24 @@
manager.registerDefaultNetworkCallback(callback);
verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
- eq(testPkgName), eq(testAttributionTag));
+ eq(testPkgName), eq(testAttributionTag), anyInt());
reset(mService);
manager.registerDefaultNetworkCallbackForUid(42, callback, handler);
verify(mService).requestNetwork(eq(42), eq(null),
eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
- eq(testPkgName), eq(testAttributionTag));
+ eq(testPkgName), eq(testAttributionTag), anyInt());
manager.requestBackgroundNetwork(request, callback, handler);
verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
eq(BACKGROUND_REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
- eq(testPkgName), eq(testAttributionTag));
+ eq(testPkgName), eq(testAttributionTag), anyInt());
reset(mService);
manager.registerSystemDefaultNetworkCallback(callback, handler);
verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
eq(TRACK_SYSTEM_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
- eq(testPkgName), eq(testAttributionTag));
+ eq(testPkgName), eq(testAttributionTag), anyInt());
reset(mService);
}
@@ -516,16 +519,154 @@
+ " attempts", ref.get());
}
- private <T> void mockService(Class<T> clazz, String name, T service) {
- doReturn(service).when(mCtx).getSystemService(name);
- doReturn(name).when(mCtx).getSystemServiceName(clazz);
+ @Test
+ public void testDeclaredMethodsFlag_requestWithMixedMethods_RegistrationFlagsMatch()
+ throws Exception {
+ doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+ .when(mService).getEnabledConnectivityManagerFeatures();
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
- // If the test suite uses the inline mock maker library, such as for coverage tests,
- // then the final version of getSystemService must also be mocked, as the real
- // method will not be called by the test and null object is returned since no mock.
- // Otherwise, mocking a final method will fail the test.
- if (mCtx.getSystemService(clazz) == null) {
- doReturn(service).when(mCtx).getSystemService(clazz);
- }
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ final NetworkCallback callback1 = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onPreCheck(@NonNull Network network) {}
+ @Override
+ public void onAvailable(@NonNull Network network) {}
+ @Override
+ public void onLost(@NonNull Network network) {}
+ @Override
+ public void onCapabilitiesChanged(@NonNull Network network,
+ @NonNull NetworkCapabilities networkCapabilities) {}
+ @Override
+ public void onLocalNetworkInfoChanged(@NonNull Network network,
+ @NonNull LocalNetworkInfo localNetworkInfo) {}
+ @Override
+ public void onNetworkResumed(@NonNull Network network) {}
+ @Override
+ public void onBlockedStatusChanged(@NonNull Network network, int blocked) {}
+ };
+ manager.requestNetwork(request, callback1);
+
+ final InOrder inOrder = inOrder(mService);
+ inOrder.verify(mService).requestNetwork(
+ anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+ eq(1 << ConnectivityManager.CALLBACK_PRECHECK
+ | 1 << ConnectivityManager.CALLBACK_AVAILABLE
+ | 1 << ConnectivityManager.CALLBACK_LOST
+ | 1 << ConnectivityManager.CALLBACK_CAP_CHANGED
+ | 1 << ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED
+ | 1 << ConnectivityManager.CALLBACK_RESUMED
+ | 1 << ConnectivityManager.CALLBACK_BLK_CHANGED));
+ }
+
+ @Test
+ public void testDeclaredMethodsFlag_listenWithMixedMethods_RegistrationFlagsMatch()
+ throws Exception {
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+ .when(mService).getEnabledConnectivityManagerFeatures();
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+ final NetworkCallback callback2 = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onLosing(@NonNull Network network, int maxMsToLive) {}
+ @Override
+ public void onUnavailable() {}
+ @Override
+ public void onLinkPropertiesChanged(@NonNull Network network,
+ @NonNull LinkProperties linkProperties) {}
+ @Override
+ public void onNetworkSuspended(@NonNull Network network) {}
+ };
+ manager.registerNetworkCallback(request, callback2);
+ // Call a second time with the same callback to exercise caching
+ manager.registerNetworkCallback(request, callback2);
+
+ verify(mService, times(2)).listenForNetwork(
+ any(), any(), any(), anyInt(), any(), any(),
+ eq(1 << ConnectivityManager.CALLBACK_LOSING
+ // AVAILABLE calls IP_CHANGED and SUSPENDED so it gets added
+ | 1 << ConnectivityManager.CALLBACK_AVAILABLE
+ | 1 << ConnectivityManager.CALLBACK_UNAVAIL
+ | 1 << ConnectivityManager.CALLBACK_IP_CHANGED
+ | 1 << ConnectivityManager.CALLBACK_SUSPENDED));
+ }
+
+ @Test
+ public void testDeclaredMethodsFlag_requestWithHiddenAvailableCallback_RegistrationFlagsMatch()
+ throws Exception {
+ doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+ .when(mService).getEnabledConnectivityManagerFeatures();
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+ final NetworkCallback hiddenOnAvailableCb = new ConnectivityManager.NetworkCallback() {
+ // This overload is @hide but might still be used by (bad) apps
+ @Override
+ public void onAvailable(@NonNull Network network,
+ @NonNull NetworkCapabilities networkCapabilities,
+ @NonNull LinkProperties linkProperties, boolean blocked) {}
+ };
+ manager.registerDefaultNetworkCallback(hiddenOnAvailableCb);
+
+ verify(mService).requestNetwork(
+ anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+ eq(1 << ConnectivityManager.CALLBACK_AVAILABLE));
+ }
+
+ public static class NetworkCallbackWithOnLostOnly extends NetworkCallback {
+ @Override
+ public void onLost(@NonNull Network network) {}
+ }
+
+ @Test
+ public void testDeclaredMethodsFlag_requestWithoutAvailableCallback_RegistrationFlagsMatch()
+ throws Exception {
+ doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+ .when(mService).getEnabledConnectivityManagerFeatures();
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ final NetworkCallback noOnAvailableCb = new NetworkCallbackWithOnLostOnly();
+ manager.registerSystemDefaultNetworkCallback(noOnAvailableCb, handler);
+
+ verify(mService).requestNetwork(
+ anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+ eq(1 << ConnectivityManager.CALLBACK_LOST));
+ }
+
+ @Test
+ public void testDeclaredMethodsFlag_listenWithMock_OptimizationDisabled()
+ throws Exception {
+ doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+ .when(mService).getEnabledConnectivityManagerFeatures();
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ manager.registerNetworkCallback(request, mock(NetworkCallbackWithOnLostOnly.class),
+ handler);
+
+ verify(mService).listenForNetwork(
+ any(), any(), any(), anyInt(), any(), any(),
+ // Mock that does not call the constructor -> do not use the optimization
+ eq(~0));
+ }
+
+ @Test
+ public void testDeclaredMethodsFlag_requestWitNoCallback_OptimizationDisabled()
+ throws Exception {
+ doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+ .when(mService).getEnabledConnectivityManagerFeatures();
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ final NetworkCallback noCallbackAtAll = new ConnectivityManager.NetworkCallback() {};
+ manager.requestBackgroundNetwork(request, noCallbackAtAll, handler);
+
+ verify(mService).requestNetwork(
+ anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+ // No callbacks overridden -> do not use the optimization
+ eq(~0));
}
}
diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java
index 0b82759..e8f91e6 100644
--- a/tests/unit/java/android/net/IpMemoryStoreTest.java
+++ b/tests/unit/java/android/net/IpMemoryStoreTest.java
@@ -16,6 +16,11 @@
package android.net;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -68,6 +73,14 @@
-128, 0, 89, 112, 91, -34 };
private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes(
"hint", 219);
+ private static final long ONE_WEEK_IN_MS = 7 * 24 * 3600 * 1000;
+ private static final long ONE_DAY_IN_MS = 24 * 3600 * 1000;
+ private static final int[] NETWORK_EVENT_NUD_FAILURES = new int[] {
+ NETWORK_EVENT_NUD_FAILURE_ROAM,
+ NETWORK_EVENT_NUD_FAILURE_CONFIRM,
+ NETWORK_EVENT_NUD_FAILURE_ORGANIC,
+ NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED
+ };
@Mock
Context mMockContext;
@@ -333,4 +346,31 @@
mStore.factoryReset();
verify(mMockService, times(1)).factoryReset();
}
+
+ @Test
+ public void testNetworkEvents() throws Exception {
+ startIpMemoryStore(true /* supplyService */);
+ final String cluster = "cluster";
+
+ final long now = System.currentTimeMillis();
+ final long expiry = now + ONE_WEEK_IN_MS;
+ mStore.storeNetworkEvent(cluster, now, expiry, NETWORK_EVENT_NUD_FAILURE_ROAM,
+ status -> assertTrue("Store not successful : " + status.resultCode,
+ status.isSuccess()));
+ verify(mMockService, times(1)).storeNetworkEvent(eq(cluster),
+ eq(now), eq(expiry), eq(NETWORK_EVENT_NUD_FAILURE_ROAM), any());
+
+ final long[] sinceTimes = new long[2];
+ sinceTimes[0] = now - ONE_WEEK_IN_MS;
+ sinceTimes[1] = now - ONE_DAY_IN_MS;
+ mStore.retrieveNetworkEventCount(cluster, sinceTimes, NETWORK_EVENT_NUD_FAILURES,
+ (status, counts) -> {
+ assertTrue("Retrieve network event counts not successful : "
+ + status.resultCode, status.isSuccess());
+ assertEquals(new int[0], counts);
+ });
+
+ verify(mMockService, times(1)).retrieveNetworkEventCount(eq(cluster), eq(sinceTimes),
+ eq(NETWORK_EVENT_NUD_FAILURES), any());
+ }
}
diff --git a/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt b/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt
new file mode 100644
index 0000000..af06a64
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.ConnectivityManager.NetworkCallbackMethodsHolder
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doCallRealMethod
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.mockingDetails
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkCallbackFlagsTest {
+
+ // To avoid developers forgetting to update NETWORK_CB_METHODS when modifying NetworkCallbacks,
+ // or using wrong values, calculate it from annotations here and verify that it matches.
+ // This avoids the runtime cost of reflection, but still ensures that the list is correct.
+ @Test
+ fun testNetworkCallbackMethods_calculateFromAnnotations_matchesHardcodedList() {
+ val calculatedMethods = getNetworkCallbackMethodsFromAnnotations()
+ assertEquals(
+ calculatedMethods.toSet(),
+ NetworkCallbackMethodsHolder.NETWORK_CB_METHODS.map {
+ NetworkCallbackMethodWithEquals(
+ it.mName,
+ it.mParameterTypes.toList(),
+ callbacksCallingThisMethod = it.mCallbacksCallingThisMethod
+ )
+ }.toSet()
+ )
+ }
+
+ data class NetworkCallbackMethodWithEquals(
+ val name: String,
+ val parameterTypes: List<Class<*>>,
+ val callbacksCallingThisMethod: Int
+ )
+
+ data class NetworkCallbackMethodBuilder(
+ val name: String,
+ val parameterTypes: List<Class<*>>,
+ val isFinal: Boolean,
+ val methodId: Int,
+ val mayCall: Set<Int>?,
+ var callbacksCallingThisMethod: Int
+ ) {
+ fun build() = NetworkCallbackMethodWithEquals(
+ name,
+ parameterTypes,
+ callbacksCallingThisMethod
+ )
+ }
+
+ /**
+ * Build [NetworkCallbackMethodsHolder.NETWORK_CB_METHODS] from [NetworkCallback] annotations.
+ */
+ private fun getNetworkCallbackMethodsFromAnnotations(): List<NetworkCallbackMethodWithEquals> {
+ val parsedMethods = mutableListOf<NetworkCallbackMethodBuilder>()
+ val methods = NetworkCallback::class.java.declaredMethods
+ methods.forEach { method ->
+ val cb = method.getAnnotation(
+ NetworkCallback.FilteredCallback::class.java
+ ) ?: return@forEach
+ val callbacksCallingThisMethod = if (cb.calledByCallbackId == 0) {
+ 0
+ } else {
+ 1 shl cb.calledByCallbackId
+ }
+ parsedMethods.add(
+ NetworkCallbackMethodBuilder(
+ method.name,
+ method.parameterTypes.toList(),
+ Modifier.isFinal(method.modifiers),
+ cb.methodId,
+ cb.mayCall.toSet(),
+ callbacksCallingThisMethod
+ )
+ )
+ }
+
+ // Propagate callbacksCallingThisMethod for transitive calls
+ do {
+ var hadChange = false
+ parsedMethods.forEach { caller ->
+ parsedMethods.forEach { callee ->
+ if (caller.mayCall?.contains(callee.methodId) == true) {
+ // Callbacks that call the caller also cause calls to the callee. So
+ // callbacksCallingThisMethod for the callee should include
+ // callbacksCallingThisMethod from the caller.
+ val newValue =
+ caller.callbacksCallingThisMethod or callee.callbacksCallingThisMethod
+ hadChange = hadChange || callee.callbacksCallingThisMethod != newValue
+ callee.callbacksCallingThisMethod = newValue
+ }
+ }
+ }
+ } while (hadChange)
+
+ // Final methods may affect the flags for transitive calls, but cannot be overridden, so do
+ // not need to be in the list (no overridden method in NetworkCallback will match them).
+ return parsedMethods.filter { !it.isFinal }.map { it.build() }
+ }
+
+ @Test
+ fun testMethodsAreAnnotated() {
+ val annotations = NetworkCallback::class.java.declaredMethods.mapNotNull { method ->
+ if (!Modifier.isPublic(method.modifiers) && !Modifier.isProtected(method.modifiers)) {
+ return@mapNotNull null
+ }
+ val annotation = method.getAnnotation(NetworkCallback.FilteredCallback::class.java)
+ assertNotNull(annotation, "$method is missing the @FilteredCallback annotation")
+ return@mapNotNull annotation
+ }
+
+ annotations.groupingBy { it.methodId }.eachCount().forEach { (methodId, cnt) ->
+ assertEquals(1, cnt, "Method ID $methodId is used more than once in @FilteredCallback")
+ }
+ }
+
+ @Test
+ fun testObviousCalleesAreInAnnotation() {
+ NetworkCallback::class.java.declaredMethods.forEach { method ->
+ val annotation = method.getAnnotation(NetworkCallback.FilteredCallback::class.java)
+ ?: return@forEach
+ val missingFlags = getObviousCallees(method).toMutableSet().apply {
+ removeAll(annotation.mayCall.toSet())
+ }
+ val msg = "@FilteredCallback on $method is missing flags " +
+ "$missingFlags in mayCall. There may be other " +
+ "calls that are not detected if they are done conditionally."
+ assertEquals(emptySet(), missingFlags, msg)
+ }
+ }
+
+ /**
+ * Invoke the specified NetworkCallback method with mock arguments, return a set of transitively
+ * called methods.
+ *
+ * This provides an idea of which methods are transitively called by the specified method. It's
+ * not perfect as some callees could be called or not depending on the exact values of the mock
+ * arguments that are passed in (for example, onAvailable calls onNetworkSuspended only if the
+ * capabilities lack the NOT_SUSPENDED capability), but it should catch obvious forgotten calls.
+ */
+ private fun getObviousCallees(method: Method): Set<Int> {
+ // Create a mock NetworkCallback that mocks all methods except the one specified by the
+ // caller.
+ val mockCallback = mock(NetworkCallback::class.java)
+
+ if (!Modifier.isFinal(method.modifiers) ||
+ // The mock class will be NetworkCallback (not a subclass) if using mockito-inline,
+ // which mocks final methods too
+ mockCallback.javaClass == NetworkCallback::class.java) {
+ doCallRealMethod().`when`(mockCallback).let { mockObj ->
+ val anyArgs = method.parameterTypes.map { any(it) }
+ method.invoke(mockObj, *anyArgs.toTypedArray())
+ }
+ }
+
+ // Invoke the target method with mock parameters
+ val mockParameters = method.parameterTypes.map { getMockFor(method, it) }
+ method.invoke(mockCallback, *mockParameters.toTypedArray())
+
+ // Aggregate callees
+ val mockingDetails = mockingDetails(mockCallback)
+ return mockingDetails.invocations.mapNotNull { inv ->
+ if (inv.method == method) {
+ null
+ } else {
+ inv.method.getAnnotation(NetworkCallback.FilteredCallback::class.java)?.methodId
+ }
+ }.toSet()
+ }
+
+ private fun getMockFor(method: Method, c: Class<*>): Any {
+ if (!c.isPrimitive && !Modifier.isFinal(c.modifiers)) {
+ return mock(c)
+ }
+ return when (c) {
+ NetworkCapabilities::class.java -> NetworkCapabilities()
+ LinkProperties::class.java -> LinkProperties()
+ LocalNetworkInfo::class.java -> LocalNetworkInfo(null)
+ Boolean::class.java -> false
+ Int::class.java -> 0
+ else -> fail("No mock set for parameter type $c used in $method")
+ }
+ }
+}
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
new file mode 100644
index 0000000..c61541e
--- /dev/null
+++ b/tests/unit/java/android/net/TrafficStatsTest.kt
@@ -0,0 +1,46 @@
+/*
+* Copyright (C) 2024 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package android.net
+
+import android.net.TrafficStats.getValueForTypeFromFirstEntry
+import android.net.TrafficStats.TYPE_RX_BYTES
+import android.net.TrafficStats.UNSUPPORTED
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+private const val TEST_IFACE1 = "test_iface1"
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class TrafficStatsTest {
+
+ @Test
+ fun testGetValueForTypeFromFirstEntry() {
+ var stats: NetworkStats = NetworkStats(0, 0)
+ // empty stats
+ assertEquals(getValueForTypeFromFirstEntry(stats, TYPE_RX_BYTES), UNSUPPORTED.toLong())
+ // invalid type
+ stats.insertEntry(TEST_IFACE1, 1, 2, 3, 4)
+ assertEquals(getValueForTypeFromFirstEntry(stats, 1000), UNSUPPORTED.toLong())
+ // valid type
+ assertEquals(getValueForTypeFromFirstEntry(stats, TYPE_RX_BYTES), 1)
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 859c54a..c1c15ca 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -41,6 +41,7 @@
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
@@ -136,6 +137,12 @@
private static final int TEST_UID = 10086;
private static final int[] TEST_UIDS = {10002, 10003};
+ private static final int[] CORE_AIDS = {
+ Process.ROOT_UID,
+ Process.SYSTEM_UID,
+ Process.FIRST_APPLICATION_UID - 10,
+ Process.FIRST_APPLICATION_UID - 1,
+ };
private static final String TEST_IF_NAME = "wlan0";
private static final int TEST_IF_INDEX = 7;
private static final int NO_IIF = 0;
@@ -1261,15 +1268,9 @@
assertTrue(BpfNetMapsUtils.isUidNetworkingBlocked(TEST_UID, false, mConfigurationMap,
mUidOwnerMap, mDataSaverEnabledMap));
- final int[] coreAids = new int[] {
- Process.ROOT_UID,
- Process.SYSTEM_UID,
- Process.FIRST_APPLICATION_UID - 10,
- Process.FIRST_APPLICATION_UID - 1,
- };
// Core appIds are not on the chain but should still be allowed on any user.
for (int userId = 0; userId < 20; userId++) {
- for (final int aid : coreAids) {
+ for (final int aid : CORE_AIDS) {
final int uid = UserHandle.getUid(userId, aid);
assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, mConfigurationMap,
mUidOwnerMap, mDataSaverEnabledMap));
@@ -1277,6 +1278,26 @@
}
}
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetUidNetworkingBlockedReasonsForCoreUids() throws Exception {
+ // Enable BACKGROUND_MATCH that is an allowlist match.
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(BACKGROUND_MATCH));
+
+ // Non-core uid that is not on this chain is blocked by BLOCKED_REASON_APP_BACKGROUND.
+ assertEquals(BLOCKED_REASON_APP_BACKGROUND, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+ TEST_UID, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+
+ // Core appIds are not on the chain but should not be blocked on any users.
+ for (int userId = 0; userId < 20; userId++) {
+ for (final int aid : CORE_AIDS) {
+ final int uid = UserHandle.getUid(userId, aid);
+ assertEquals(BLOCKED_REASON_NONE, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+ uid, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+ }
+ }
+ }
+
private void doTestIsUidRestrictedOnMeteredNetworks(
final long enabledMatches,
final long uidRules,
diff --git a/tests/unit/java/com/android/server/CallbackQueueTest.kt b/tests/unit/java/com/android/server/CallbackQueueTest.kt
new file mode 100644
index 0000000..d8d35c1
--- /dev/null
+++ b/tests/unit/java/com/android/server/CallbackQueueTest.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.net.ConnectivityManager
+import android.net.ConnectivityManager.CALLBACK_AVAILABLE
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_NETID_1 = 123
+
+// Maximum 16 bits unsigned value
+private const val TEST_NETID_2 = 0xffff
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CallbackQueueTest {
+ @Test
+ fun testAddCallback() {
+ val cbs = listOf(
+ TEST_NETID_1 to CALLBACK_AVAILABLE,
+ TEST_NETID_2 to CALLBACK_AVAILABLE,
+ TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+ TEST_NETID_1 to CALLBACK_CAP_CHANGED
+ )
+ val queue = CallbackQueue(intArrayOf()).apply {
+ cbs.forEach { addCallback(it.first, it.second) }
+ }
+
+ assertQueueEquals(cbs, queue)
+ }
+
+ @Test
+ fun testHasCallback() {
+ val queue = CallbackQueue(intArrayOf()).apply {
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+ addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+ }
+
+ assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_AVAILABLE))
+ assertTrue(queue.hasCallback(TEST_NETID_2, CALLBACK_AVAILABLE))
+ assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED))
+
+ assertFalse(queue.hasCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED))
+ assertFalse(queue.hasCallback(1234, CALLBACK_AVAILABLE))
+ assertFalse(queue.hasCallback(TEST_NETID_1, 5678))
+ assertFalse(queue.hasCallback(1234, 5678))
+ }
+
+ @Test
+ fun testRemoveCallbacks() {
+ val queue = CallbackQueue(intArrayOf()).apply {
+ assertFalse(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+ addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ assertTrue(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+ }
+ assertQueueEquals(listOf(
+ TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+ TEST_NETID_2 to CALLBACK_AVAILABLE
+ ), queue)
+ }
+
+ @Test
+ fun testRemoveCallbacksForNetId() {
+ val queue = CallbackQueue(intArrayOf()).apply {
+ assertFalse(removeCallbacksForNetId(TEST_NETID_2))
+ addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+ assertFalse(removeCallbacksForNetId(TEST_NETID_1))
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+ addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_2, CALLBACK_IP_CHANGED)
+ assertTrue(removeCallbacksForNetId(TEST_NETID_2))
+ }
+ assertQueueEquals(listOf(
+ TEST_NETID_1 to CALLBACK_AVAILABLE,
+ TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+ TEST_NETID_1 to CALLBACK_AVAILABLE,
+ ), queue)
+ }
+
+ @Test
+ fun testConstructorFromExistingArray() {
+ val queue1 = CallbackQueue(intArrayOf()).apply {
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+ }
+ val queue2 = CallbackQueue(queue1.minimizedBackingArray)
+ assertQueueEquals(listOf(
+ TEST_NETID_1 to CALLBACK_AVAILABLE,
+ TEST_NETID_2 to CALLBACK_AVAILABLE
+ ), queue2)
+ }
+
+ @Test
+ fun testToString() {
+ assertEquals("[]", CallbackQueue(intArrayOf()).toString())
+ assertEquals(
+ "[CALLBACK_AVAILABLE($TEST_NETID_1)]",
+ CallbackQueue(intArrayOf()).apply {
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ }.toString()
+ )
+ assertEquals(
+ "[CALLBACK_AVAILABLE($TEST_NETID_1),CALLBACK_CAP_CHANGED($TEST_NETID_2)]",
+ CallbackQueue(intArrayOf()).apply {
+ addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+ addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+ }.toString()
+ )
+ }
+
+ @Test
+ fun testMaxNetId() {
+ // CallbackQueue assumes netIds are at most 16 bits
+ assertTrue(NetIdManager.MAX_NET_ID <= 0xffff)
+ }
+
+ @Test
+ fun testMaxCallbackId() {
+ // CallbackQueue assumes callback IDs are at most 16 bits.
+ val constants = ConnectivityManager::class.java.declaredFields.filter {
+ Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
+ it.name.startsWith("CALLBACK_")
+ }
+ constants.forEach {
+ it.isAccessible = true
+ assertTrue(it.get(null) as Int <= 0xffff)
+ }
+ }
+}
+
+private fun assertQueueEquals(expected: List<Pair<Int, Int>>, actual: CallbackQueue) {
+ assertEquals(
+ expected.size,
+ actual.length(),
+ "Size mismatch between expected: $expected and actual: $actual"
+ )
+
+ var nextIndex = 0
+ actual.forEach { netId, cbId ->
+ val (expNetId, expCbId) = expected[nextIndex]
+ val msg = "$actual does not match $expected at index $nextIndex"
+ assertEquals(expNetId, netId, msg)
+ assertEquals(expCbId, cbId, msg)
+ nextIndex++
+ }
+ // Ensure forEach iterations and size are consistent
+ assertEquals(expected.size, nextIndex)
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8526a9a..999d17d 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2180,6 +2180,7 @@
case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+ case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
return true;
default:
@@ -2871,7 +2872,7 @@
};
final NetworkRequest request = mService.listenForNetwork(caps, messenger, binder,
NetworkCallback.FLAG_NONE, mContext.getOpPackageName(),
- mContext.getAttributionTag());
+ mContext.getAttributionTag(), ~0 /* declaredMethodsFlag */);
mService.releaseNetworkRequest(request);
deathRecipient.get().binderDied();
// Wait for the release message to be processed.
@@ -5407,7 +5408,7 @@
mService.requestNetwork(Process.INVALID_UID, networkCapabilities,
NetworkRequest.Type.REQUEST.ordinal(), null, 0, null,
ConnectivityManager.TYPE_WIFI, NetworkCallback.FLAG_NONE,
- mContext.getPackageName(), getAttributionTag());
+ mContext.getPackageName(), getAttributionTag(), ~0 /* declaredMethodsFlag */);
});
final NetworkRequest.Builder builder =
@@ -13655,7 +13656,8 @@
IllegalArgumentException.class,
() -> mService.requestNetwork(Process.INVALID_UID, nc, reqTypeInt, null, 0,
null, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE,
- mContext.getPackageName(), getAttributionTag())
+ mContext.getPackageName(), getAttributionTag(),
+ ~0 /* declaredMethodsFlag */)
);
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index ea3d2dd..fb3004a3 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -54,6 +54,7 @@
import android.net.ResolverOptionsParcel;
import android.net.ResolverParamsParcel;
import android.net.RouteInfo;
+import android.net.resolv.aidl.DohParamsParcel;
import android.net.shared.PrivateDnsConfig;
import android.os.Build;
import android.provider.Settings;
@@ -324,11 +325,10 @@
assertEquals(new InetAddress[0], cfgStrict.ips);
}
- @Test
- public void testSendDnsConfiguration() throws Exception {
+ private void doTestSendDnsConfiguration(PrivateDnsConfig cfg, DohParamsParcel expectedDohParams)
+ throws Exception {
reset(mMockDnsResolver);
- mDnsManager.updatePrivateDns(new Network(TEST_NETID),
- mDnsManager.getPrivateDnsConfig());
+ mDnsManager.updatePrivateDns(new Network(TEST_NETID), cfg);
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(TEST_IFACENAME);
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
@@ -352,12 +352,63 @@
expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
expectedParams.resolverOptions = null;
expectedParams.meteredNetwork = true;
- expectedParams.dohParams = null;
+ expectedParams.dohParams = expectedDohParams;
expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
}
@Test
+ public void testSendDnsConfiguration_ddrDisabled() throws Exception {
+ final PrivateDnsConfig cfg = new PrivateDnsConfig(
+ PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+ null /* hostname */,
+ null /* ips */,
+ false /* ddrEnabled */,
+ null /* dohName */,
+ null /* dohIps */,
+ null /* dohPath */,
+ -1 /* dohPort */);
+ doTestSendDnsConfiguration(cfg, null /* expectedDohParams */);
+ }
+
+ @Test
+ public void testSendDnsConfiguration_ddrEnabledEmpty() throws Exception {
+ final PrivateDnsConfig cfg = new PrivateDnsConfig(
+ PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+ null /* hostname */,
+ null /* ips */,
+ true /* ddrEnabled */,
+ null /* dohName */,
+ null /* dohIps */,
+ null /* dohPath */,
+ -1 /* dohPort */);
+
+ final DohParamsParcel params = new DohParamsParcel.Builder().build();
+ doTestSendDnsConfiguration(cfg, params);
+ }
+
+ @Test
+ public void testSendDnsConfiguration_ddrEnabled() throws Exception {
+ final PrivateDnsConfig cfg = new PrivateDnsConfig(
+ PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+ null /* hostname */,
+ null /* ips */,
+ true /* ddrEnabled */,
+ "doh.com" /* dohName */,
+ null /* dohIps */,
+ "/some-path{?dns}" /* dohPath */,
+ 5353 /* dohPort */);
+
+ final DohParamsParcel params = new DohParamsParcel.Builder()
+ .setName("doh.com")
+ .setDohpath("/some-path{?dns}")
+ .setPort(5353)
+ .build();
+
+ doTestSendDnsConfiguration(cfg, params);
+ }
+
+ @Test
public void testTransportTypesEqual() throws Exception {
SparseArray<String> ncTransTypes = MessageUtils.findMessageNames(
new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index b5c0132..ab2fb99 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,11 +19,14 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,10 +35,12 @@
import android.net.Network;
import android.os.Handler;
import android.os.HandlerThread;
+import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.Pair;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.MdnsDiscoveryManager.DiscoveryExecutor;
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -55,7 +60,9 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
/** Tests for {@link MdnsDiscoveryManager}. */
@DevSdkIgnoreRunner.MonitorThreadLeak
@@ -96,6 +103,7 @@
@Mock MdnsServiceBrowserListener mockListenerOne;
@Mock MdnsServiceBrowserListener mockListenerTwo;
@Mock SharedLog sharedLog;
+ @Mock MdnsServiceCache mockServiceCache;
private MdnsDiscoveryManager discoveryManager;
private HandlerThread thread;
private Handler handler;
@@ -139,7 +147,9 @@
return null;
}
};
+ discoveryManager = makeDiscoveryManager(MdnsFeatureFlags.newBuilder().build());
doReturn(mockExecutorService).when(mockServiceTypeClientType1NullNetwork).getExecutor();
+ doReturn(mockExecutorService).when(mockServiceTypeClientType1Network1).getExecutor();
}
@After
@@ -150,6 +160,40 @@
}
}
+ private MdnsDiscoveryManager makeDiscoveryManager(@NonNull MdnsFeatureFlags featureFlags) {
+ return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog, featureFlags) {
+ @Override
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
+ @NonNull SocketKey socketKey) {
+ createdServiceTypeClientCount++;
+ final Pair<String, SocketKey> perSocketServiceType =
+ Pair.create(serviceType, socketKey);
+ if (perSocketServiceType.equals(PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK)) {
+ return mockServiceTypeClientType1NullNetwork;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_1_NETWORK_1)) {
+ return mockServiceTypeClientType1Network1;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NULL_NETWORK)) {
+ return mockServiceTypeClientType2NullNetwork;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NETWORK_1)) {
+ return mockServiceTypeClientType2Network1;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NETWORK_2)) {
+ return mockServiceTypeClientType2Network2;
+ }
+ fail("Unexpected perSocketServiceType: " + perSocketServiceType);
+ return null;
+ }
+
+ @Override
+ MdnsServiceCache getServiceCache() {
+ return mockServiceCache;
+ }
+ };
+ }
+
private void runOnHandler(Runnable r) {
handler.post(r);
HandlerUtils.waitForIdle(handler, DEFAULT_TIMEOUT);
@@ -390,6 +434,99 @@
verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
}
+ @Test
+ public void testDiscoveryExecutor() throws Exception {
+ final TestableLooper testableLooper = new TestableLooper(thread.getLooper());
+ final DiscoveryExecutor executor = new DiscoveryExecutor(testableLooper.getLooper());
+ try {
+ // Verify the checkAndRunOnHandlerThread method
+ final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
+ executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
+ assertTrue(future1.isDone());
+ assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Verify the execute method
+ final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
+ executor.execute(()-> future2.complete(true));
+ testableLooper.processAllMessages();
+ assertTrue(future2.isDone());
+ assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Verify the executeDelayed method
+ final CompletableFuture<Boolean> future3 = new CompletableFuture<>();
+ // Schedule a task with 999 ms delay
+ executor.executeDelayed(()-> future3.complete(true), 999L);
+ testableLooper.processAllMessages();
+ assertFalse(future3.isDone());
+
+ // 500 ms have elapsed but do not exceed the target time (999 ms)
+ // The function should not be executed.
+ testableLooper.moveTimeForward(500L);
+ testableLooper.processAllMessages();
+ assertFalse(future3.isDone());
+
+ // 500 ms have elapsed again and have exceeded the target time (999 ms).
+ // The function should be executed.
+ testableLooper.moveTimeForward(500L);
+ testableLooper.processAllMessages();
+ assertTrue(future3.isDone());
+ assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
+ } finally {
+ testableLooper.destroy();
+ }
+ }
+
+ @Test
+ public void testRemoveServicesAfterAllListenersUnregistered() throws IOException {
+ final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
+ .setIsCachedServicesRemovalEnabled(true)
+ .setCachedServicesRetentionTime(0L)
+ .build();
+ discoveryManager = makeDiscoveryManager(mdnsFeatureFlags);
+
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final MdnsServiceCache.CacheKey cacheKey =
+ new MdnsServiceCache.CacheKey(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+ doReturn(cacheKey).when(mockServiceTypeClientType1Network1).getCacheKey();
+ doReturn(true).when(mockServiceTypeClientType1Network1)
+ .stopSendAndReceive(mockListenerOne);
+ runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
+ verify(executorProvider).shutdownExecutorService(mockExecutorService);
+ verify(mockServiceTypeClientType1Network1).stopSendAndReceive(mockListenerOne);
+ verify(socketClient).stopDiscovery();
+ verify(mockServiceCache, timeout(DEFAULT_TIMEOUT)).removeServices(cacheKey);
+ }
+
+ @Test
+ public void testRemoveServicesAfterSocketDestroyed() throws IOException {
+ final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
+ .setIsCachedServicesRemovalEnabled(true)
+ .setCachedServicesRetentionTime(0L)
+ .build();
+ discoveryManager = makeDiscoveryManager(mdnsFeatureFlags);
+
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final MdnsServiceCache.CacheKey cacheKey =
+ new MdnsServiceCache.CacheKey(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+ doReturn(cacheKey).when(mockServiceTypeClientType1Network1).getCacheKey();
+ runOnHandler(() -> callback.onSocketDestroyed(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).notifySocketDestroyed();
+ verify(executorProvider).shutdownExecutorService(mockExecutorService);
+ verify(mockServiceCache, timeout(DEFAULT_TIMEOUT)).removeServices(cacheKey);
+ }
+
private MdnsPacket createMdnsPacket(String serviceType) {
final String[] type = TextUtils.split(serviceType, "\\.");
final ArrayList<String> name = new ArrayList<>(type.length + 1);
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 fb3d183..4c71991 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -18,8 +18,10 @@
import static com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback;
import static com.android.server.connectivity.mdns.MulticastPacketReader.PacketHandler;
+import static com.android.testutils.Cleanup.testAndCleanup;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
@@ -35,6 +37,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
+import android.util.Log;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.SharedLog;
@@ -59,6 +62,7 @@
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -437,4 +441,34 @@
inOrder.verify(mSocket).send(packets.get(i));
}
}
+
+ @Test
+ public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+ final SocketCallback callback = expectSocketCallback();
+ final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+ InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+ final DatagramPacket ipv6Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+ InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+ doReturn(true).when(mSocket).hasJoinedIpv4();
+ doReturn(true).when(mSocket).hasJoinedIpv6();
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+ // Notify socket created
+ callback.onSocketCreated(mSocketKey, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+
+ // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+ // Override the default TerribleFailureHandler, as that handler might terminate the process
+ // (if we're on an eng build).
+ final AtomicBoolean hasFailed = new AtomicBoolean(false);
+ final Log.TerribleFailureHandler originalHandler =
+ Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+ testAndCleanup(() -> {
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+ mSocketKey, false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ assertTrue(hasFailed.get());
+ verify(mSocket, never()).send(any());
+ }, () -> Log.setWtfHandler(originalHandler));
+ }
}
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 2cb97c9..9674da3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -123,18 +123,18 @@
}
private val TEST_PUBLIC_KEY = hexStringToByteArray(
- "0201030dc141d0637960b98cbc12cfca"
- + "221d2879dac26ee5b460e9007c992e19"
- + "02d897c391b03764d448f7d0c772fdb0"
- + "3b1d9d6d52ff8886769e8e2362513565"
- + "270962d3")
+ "0201030dc141d0637960b98cbc12cfca" +
+ "221d2879dac26ee5b460e9007c992e19" +
+ "02d897c391b03764d448f7d0c772fdb0" +
+ "3b1d9d6d52ff8886769e8e2362513565" +
+ "270962d3")
private val TEST_PUBLIC_KEY_2 = hexStringToByteArray(
- "0201030dc141d0637960b98cbc12cfca"
- + "221d2879dac26ee5b460e9007c992e19"
- + "02d897c391b03764d448f7d0c772fdb0"
- + "3b1d9d6d52ff8886769e8e2362513565"
- + "270962d4")
+ "0201030dc141d0637960b98cbc12cfca" +
+ "221d2879dac26ee5b460e9007c992e19" +
+ "02d897c391b03764d448f7d0c772fdb0" +
+ "3b1d9d6d52ff8886769e8e2362513565" +
+ "270962d4")
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -159,7 +159,7 @@
@Before
fun setUp() {
- deps.resetElapsedRealTime();
+ deps.resetElapsedRealTime()
thread.start()
}
@@ -172,11 +172,13 @@
private fun makeFlags(
includeInetAddressesInProbing: Boolean = false,
isKnownAnswerSuppressionEnabled: Boolean = false,
- unicastReplyEnabled: Boolean = true
+ unicastReplyEnabled: Boolean = true,
+ avoidAdvertisingEmptyTxtRecords: Boolean = true
) = MdnsFeatureFlags.Builder()
.setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
.setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
.setIsUnicastReplyEnabled(unicastReplyEnabled)
+ .setAvoidAdvertisingEmptyTxtRecords(avoidAdvertisingEmptyTxtRecords)
.build()
@Test
@@ -1721,6 +1723,30 @@
}
@Test
+ fun testGetConflictingServices_ZeroLengthTxtRecord_NoConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsTextRecord(
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ listOf(TextEntry("", null as ByteArray?))
+ ),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */
+ )
+
+ assertEquals(emptyMap(), repository.getConflictingServices(packet))
+ }
+
+ @Test
fun testGetServiceRepliedRequestsCount() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
@@ -2168,6 +2194,46 @@
assertEquals(knownAnswers, reply.knownAnswers)
}
+ private fun doAddServiceWithEmptyTxtRecordTest(flags: MdnsFeatureFlags): MdnsTextRecord {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ val questions = listOf(MdnsTextRecord(
+ arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+ true /* isUnicast */
+ ))
+ 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)
+
+ assertNotNull(reply)
+ assertEquals(1, reply.answers.size)
+ assertTrue(reply.answers[0] is MdnsTextRecord)
+ return reply.answers[0] as MdnsTextRecord
+ }
+
+ @Test
+ fun testAddService_AvoidEmptyTxtRecords_HasTxtRecordWithEmptyString() {
+ val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags())
+ assertEquals(1, answerRecord.entries.size)
+ assertEquals(0, answerRecord.entries[0].key.length)
+ assertNull(answerRecord.entries[0].value)
+ }
+
+ @Test
+ fun testAddService_UsesEmptyTxtRecords_HasEmptyTxtRecord() {
+ val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags(
+ avoidAdvertisingEmptyTxtRecords = false
+ ))
+ assertEquals(0, answerRecord.entries.size)
+ }
+
@Test
fun testRestartProbingForHostname() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
index 63548c1..784c502 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -28,6 +28,8 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static java.util.Collections.emptyList;
+
import android.util.Log;
import com.android.net.module.util.HexDump;
@@ -372,6 +374,30 @@
assertEquals(dataInText, dataOutText);
}
+ private static MdnsTextRecord makeTextRecordWithEntries(List<TextEntry> entries) {
+ return new MdnsTextRecord(new String[] { "test", "record" }, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, 120_000L /* ttlMillis */, entries);
+ }
+
+ @Test
+ public void testTextRecord_EmptyRecordsAreEquivalent() {
+ final MdnsTextRecord record1 = makeTextRecordWithEntries(emptyList());
+ final MdnsTextRecord record2 = makeTextRecordWithEntries(
+ List.of(new TextEntry("", (byte[]) null)));
+ final MdnsTextRecord record3 = makeTextRecordWithEntries(
+ List.of(new TextEntry(null, (byte[]) null)));
+ final MdnsTextRecord nonEmptyRecord = makeTextRecordWithEntries(
+ List.of(new TextEntry("a", (byte[]) null)));
+
+ assertEquals(record1, record1);
+ assertEquals(record1, record2);
+ assertEquals(record1, record3);
+
+ assertNotEquals(nonEmptyRecord, record1);
+ assertNotEquals(nonEmptyRecord, record2);
+ assertNotEquals(nonEmptyRecord, record3);
+ }
+
private static String toHex(MdnsRecord record) throws IOException {
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b040ab6..0a8f108 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -154,6 +154,11 @@
serviceCache.registerServiceExpiredCallback(cacheKey, callback)
}
+ private fun removeServices(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey
+ ): Unit = runningOnHandlerAndReturn { serviceCache.removeServices(cacheKey) }
+
@Test
fun testAddAndRemoveService() {
val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
@@ -291,6 +296,37 @@
assertEquals(response4, responses[3])
}
+ @Test
+ fun testRemoveServices() {
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_1, SERVICE_TYPE_2))
+ val responses1 = getServices(serviceCache, cacheKey1)
+ assertEquals(2, responses1.size)
+ assertTrue(responses1.stream().anyMatch { response ->
+ response.serviceInstanceName == SERVICE_NAME_1
+ })
+ assertTrue(responses1.any { response ->
+ response.serviceInstanceName == SERVICE_NAME_2
+ })
+ val responses2 = getServices(serviceCache, cacheKey2)
+ assertEquals(1, responses2.size)
+ assertTrue(responses2.stream().anyMatch { response ->
+ response.serviceInstanceName == SERVICE_NAME_1
+ })
+
+ removeServices(serviceCache, cacheKey1)
+ val responses3 = getServices(serviceCache, cacheKey1)
+ assertEquals(0, responses3.size)
+ val responses4 = getServices(serviceCache, cacheKey2)
+ assertEquals(1, responses4.size)
+
+ removeServices(serviceCache, cacheKey2)
+ val responses5 = getServices(serviceCache, cacheKey2)
+ assertEquals(0, responses5.size)
+ }
+
private fun createResponse(
serviceInstanceName: String,
serviceType: String,
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 44fa55c..da0bc88 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -562,10 +562,7 @@
//MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
- QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getQueryMode(),
- false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
- socketKey);
+ QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
@@ -574,14 +571,14 @@
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
// This is the first query of a new burst. We will ask for unicast response.
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertTrue(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -590,10 +587,7 @@
public void testQueryTaskConfig_askForUnicastInFirstQuery() {
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
- QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getQueryMode(),
- false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
- socketKey);
+ QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
@@ -602,14 +596,14 @@
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
// This is the first query of a new burst. We will NOT ask for unicast response.
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -2062,6 +2056,64 @@
assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
}
+ @Test
+ public void sendQueries_AggressiveQueryMode_ServiceInCache() {
+ final int numOfQueriesBeforeBackoff = 11;
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .setQueryMode(AGGRESSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+ .build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ 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);
+ assertEquals((long) (TEST_TTL / 2 * 0.8), latestDelayMs);
+
+ // Register another listener. There is a service in cache, the query time should be
+ // rescheduled with previous run.
+ currentTime += (long) ((TEST_TTL / 2 * 0.8) - 500L);
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ startSendAndReceive(mockListenerTwo, searchOptions);
+ verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ assertNotNull(delayMessage);
+ assertEquals(500L, latestDelayMs);
+
+ // Stop all listeners
+ stopSendAndReceive(mockListenerOne);
+ stopSendAndReceive(mockListenerTwo);
+ verify(mockDeps, times(4)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // Register a new listener. There is a service in cache, the query time should be
+ // rescheduled with remaining ttl.
+ currentTime += 400L;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ assertNotNull(delayMessage);
+ assertEquals(9680L, latestDelayMs);
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 1989ed3..ab70e38 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertFalse;
@@ -38,9 +39,11 @@
import android.annotation.RequiresPermission;
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.InetAddresses;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.text.format.DateUtils;
+import android.util.Log;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.SharedLog;
@@ -594,6 +597,29 @@
}
}
+ @Test
+ public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+ mdnsClient.startDiscovery();
+ final byte[] buffer = new byte[10];
+ final DatagramPacket ipv4Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+ InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+ final DatagramPacket ipv6Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+ InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+
+ // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+ // Override the default TerribleFailureHandler, as that handler might terminate the process
+ // (if we're on an eng build).
+ final AtomicBoolean hasFailed = new AtomicBoolean(false);
+ final Log.TerribleFailureHandler originalHandler =
+ Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+ testAndCleanup(() -> {
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+ false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+ assertTrue(hasFailed.get());
+ verify(mockMulticastSocket, never()).send(any());
+ }, () -> Log.setWtfHandler(originalHandler));
+ }
+
private DatagramPacket getTestDatagramPacket() {
return new DatagramPacket(buf, 0, 5,
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), 5353 /* port */));
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index 009205e..5c3ad22 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -16,24 +16,22 @@
package com.android.server.connectivity.mdns.util
+import android.net.InetAddresses
import android.os.Build
import com.android.server.connectivity.mdns.MdnsConstants
import com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED
+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.MdnsPacket
import com.android.server.connectivity.mdns.MdnsPacketReader
import com.android.server.connectivity.mdns.MdnsPointerRecord
import com.android.server.connectivity.mdns.MdnsRecord
import com.android.server.connectivity.mdns.util.MdnsUtils.createQueryDatagramPackets
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.net.DatagramPacket
import kotlin.test.assertContentEquals
-import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -43,59 +41,6 @@
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsUtilsTest {
- @Test
- fun testToDnsLowerCase() {
- assertEquals("test", toDnsLowerCase("TEST"))
- assertEquals("test", toDnsLowerCase("TeSt"))
- assertEquals("test", toDnsLowerCase("test"))
- assertEquals("tÉst", toDnsLowerCase("TÉST"))
- assertEquals("ţést", toDnsLowerCase("ţést"))
- // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
- // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
- assertEquals(
- "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
- toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
- )
- // Also test some characters where the first surrogate is not \ud800
- assertEquals(
- "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
- toDnsLowerCase(
- "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
- )
- )
- }
-
- @Test
- fun testToDnsLabelsLowerCase() {
- assertArrayEquals(
- arrayOf("test", "tÉst", "ţést"),
- toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést"))
- )
- }
-
- @Test
- fun testEqualsIgnoreDnsCase() {
- assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
- assertTrue(equalsIgnoreDnsCase("TEST", "test"))
- assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
- assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
- assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
- // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
- // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
- assertTrue(equalsIgnoreDnsCase(
- "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
- "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
- ))
- // Also test some characters where the first surrogate is not \ud800
- assertTrue(equalsIgnoreDnsCase(
- "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
- "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
- ))
- }
@Test
fun testTruncateServiceName() {
@@ -104,14 +49,6 @@
}
@Test
- fun testEqualsLabelIgnoreDnsCase() {
- assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
- assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
- assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
- assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
- }
-
- @Test
fun testTypeEqualsOrIsSubtype() {
assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
arrayOf("_type", "_tcp", "local"),
@@ -193,4 +130,31 @@
}
return MdnsPacket(flags, questions, answers, emptyList(), emptyList())
}
+
+ @Test
+ fun testCheckAllPacketsWithSameAddress() {
+ val buffer = ByteArray(10)
+ val v4Packet = DatagramPacket(buffer, buffer.size, IPV4_SOCKET_ADDR)
+ val otherV4Packet = DatagramPacket(
+ buffer,
+ buffer.size,
+ InetAddresses.parseNumericAddress("192.0.2.1"),
+ 1234
+ )
+ val v6Packet = DatagramPacket(ByteArray(10), 10, IPV6_SOCKET_ADDR)
+ val otherV6Packet = DatagramPacket(
+ buffer,
+ buffer.size,
+ InetAddresses.parseNumericAddress("2001:db8::"),
+ 1234
+ )
+ assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf()))
+ assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet)))
+ assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v4Packet)))
+ assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, otherV4Packet)))
+ assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet)))
+ assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, v6Packet)))
+ assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, otherV6Packet)))
+ assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v6Packet)))
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
index 3ad8de8..985d403 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
@@ -419,4 +419,30 @@
deps.setChangeIdEnabled(true, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = true)
}
+
+ private fun doTestEnforceMeteredApnPolicy(restricted: Boolean) {
+ doReturn(restricted).`when`(bpfNetMaps).isUidRestrictedOnMeteredNetworks(Process.myUid())
+
+ val cellAgent = Agent(nc = cellNc())
+ cellAgent.connect()
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(cellRequest(), cb)
+
+ if (restricted) {
+ waitForIdle()
+ cb.assertNoCallback()
+ } else {
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ }
+ }
+
+ @Test
+ fun testEnforceMeteredApnPolicy_restricted() {
+ doTestEnforceMeteredApnPolicy(restricted = true)
+ }
+
+ @Test
+ fun testEnforceMeteredApnPolicy_notRestricted() {
+ doTestEnforceMeteredApnPolicy(restricted = false)
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
new file mode 100644
index 0000000..a7083dc
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.connectivityservice
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.CALLBACK_AVAILABLE
+import android.net.ConnectivityManager.CALLBACK_BLK_CHANGED
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED
+import android.net.ConnectivityManager.CALLBACK_LOST
+import android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL
+import android.net.LinkAddress
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkRequest
+import android.os.Build
+import com.android.net.module.util.BitUtils.packBits
+import com.android.server.CSTest
+import com.android.server.ConnectivityService
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
+import java.lang.reflect.Modifier
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.spy
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSDeclaredMethodsForCallbacksTest : CSTest() {
+ private val mockedCallbackFlags = AtomicInteger(DECLARED_METHODS_ALL)
+ private lateinit var wrappedService: ConnectivityService
+
+ private val instrumentedCm by lazy { ConnectivityManager(context, wrappedService) }
+
+ @Before
+ fun setUpWrappedService() {
+ // Mock the callback flags set by ConnectivityManager when calling ConnectivityService, to
+ // simulate methods not being overridden
+ wrappedService = spy(service)
+ doAnswer { inv ->
+ service.requestNetwork(
+ inv.getArgument(0),
+ inv.getArgument(1),
+ inv.getArgument(2),
+ inv.getArgument(3),
+ inv.getArgument(4),
+ inv.getArgument(5),
+ inv.getArgument(6),
+ inv.getArgument(7),
+ inv.getArgument(8),
+ inv.getArgument(9),
+ mockedCallbackFlags.get())
+ }.`when`(wrappedService).requestNetwork(
+ anyInt(),
+ any(),
+ anyInt(),
+ any(),
+ anyInt(),
+ any(),
+ anyInt(),
+ anyInt(),
+ any(),
+ any(),
+ anyInt()
+ )
+ doAnswer { inv ->
+ service.listenForNetwork(
+ inv.getArgument(0),
+ inv.getArgument(1),
+ inv.getArgument(2),
+ inv.getArgument(3),
+ inv.getArgument(4),
+ inv.getArgument(5),
+ mockedCallbackFlags.get()
+ )
+ }.`when`(wrappedService)
+ .listenForNetwork(any(), any(), any(), anyInt(), any(), any(), anyInt())
+ }
+
+ @Test
+ fun testCallbacksAreFiltered() {
+ val requestCb = TestableNetworkCallback()
+ val listenCb = TestableNetworkCallback()
+ mockedCallbackFlags.withFlags(CALLBACK_IP_CHANGED, CALLBACK_LOST) {
+ instrumentedCm.requestNetwork(NetworkRequest.Builder().build(), requestCb)
+ }
+ mockedCallbackFlags.withFlags(CALLBACK_CAP_CHANGED) {
+ instrumentedCm.registerNetworkCallback(NetworkRequest.Builder().build(), listenCb)
+ }
+
+ with(Agent()) {
+ connect()
+ sendLinkProperties(defaultLp().apply {
+ addLinkAddress(LinkAddress("fe80:db8::123/64"))
+ })
+ sendNetworkCapabilities(defaultNc().apply {
+ addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ })
+ disconnect()
+ }
+ waitForIdle()
+
+ // Only callbacks for the corresponding flags are called
+ requestCb.expect<CallbackEntry.LinkPropertiesChanged>()
+ requestCb.expect<CallbackEntry.Lost>()
+ requestCb.assertNoCallback(timeoutMs = 0L)
+
+ listenCb.expect<CallbackEntry.CapabilitiesChanged>()
+ listenCb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testDeclaredMethodsFlagsToString() {
+ assertEquals("NONE", ConnectivityService.declaredMethodsFlagsToString(0))
+ assertEquals("ALL", ConnectivityService.declaredMethodsFlagsToString(0.inv()))
+ assertEquals("AVAIL|NC|LP|BLK|LOCALINF", ConnectivityService.declaredMethodsFlagsToString(
+ (1 shl CALLBACK_AVAILABLE) or
+ (1 shl CALLBACK_CAP_CHANGED) or
+ (1 shl CALLBACK_IP_CHANGED) or
+ (1 shl CALLBACK_BLK_CHANGED) or
+ (1 shl CALLBACK_LOCAL_NETWORK_INFO_CHANGED)
+ ))
+
+ // EXPIRE_LEGACY_REQUEST (=8) is only used in ConnectivityManager and not included.
+ // CALLBACK_TRANSITIVE_CALLS_ONLY (=0) is not a callback so not included either.
+ assertEquals(
+ "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|0x7fffe101",
+ ConnectivityService.declaredMethodsFlagsToString(0x7fff_ffff)
+ )
+ // The toString method and the assertion above need to be updated if constants are added
+ val constants = ConnectivityManager::class.java.declaredFields.filter {
+ Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
+ it.name.startsWith("CALLBACK_")
+ }
+ assertEquals(12, constants.size)
+ }
+}
+
+private fun AtomicInteger.withFlags(vararg flags: Int, action: () -> Unit) {
+ tryTest {
+ set(packBits(flags).toInt())
+ action()
+ } cleanup {
+ set(DECLARED_METHODS_ALL)
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
index 93f6e81..77b06b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -26,7 +26,9 @@
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
+import android.net.VpnManager.TYPE_VPN_OEM
import android.net.VpnManager.TYPE_VPN_SERVICE
+import android.net.VpnManager.TYPE_VPN_LEGACY
import android.net.VpnTransportInfo
import android.os.Build
import androidx.test.filters.SmallTest
@@ -49,19 +51,19 @@
private const val TIMEOUT_MS = 1_000L
private const val LONG_TIMEOUT_MS = 5_000
-private fun vpnNc() = NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_VPN)
- .removeCapability(NET_CAPABILITY_NOT_VPN)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .setTransportInfo(
- VpnTransportInfo(
- TYPE_VPN_SERVICE,
- "MySession12345",
- false /* bypassable */,
- false /* longLivedTcpConnectionsExpensive */
- )
- )
- .build()
+private fun vpnNc(vpnType: Int = TYPE_VPN_SERVICE) = NetworkCapabilities.Builder().apply {
+ addTransportType(TRANSPORT_VPN)
+ removeCapability(NET_CAPABILITY_NOT_VPN)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ setTransportInfo(
+ VpnTransportInfo(
+ vpnType,
+ "MySession12345",
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */
+ )
+ )
+}.build()
private fun wifiNc() = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_WIFI)
@@ -310,4 +312,38 @@
// IngressDiscardRule should not be added since feature is disabled
verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
}
+
+ fun doTestVpnIngressDiscardRule_VpnType(vpnType: Int, expectAddRule: Boolean) {
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc(vpnType)
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ if (expectAddRule) {
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ } else {
+ verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+ }
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_ServiceVpn() {
+ doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_SERVICE, expectAddRule = true)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_LegacyVpn() {
+ // IngressDiscardRule should not be added to Legacy VPN
+ doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_LEGACY, expectAddRule = false)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_OemVpn() {
+ // IngressDiscardRule should not be added to OEM VPN
+ doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_OEM, expectAddRule = false)
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
new file mode 100644
index 0000000..fc2a06c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
@@ -0,0 +1,636 @@
+/*
+ * 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.connectivityservice
+
+import android.app.ActivityManager.UidFrozenStateChangedCallback
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN
+import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
+import android.net.ConnectivityManager.BLOCKED_REASON_NONE
+import android.net.ConnectivitySettingsManager
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkPolicyManager.NetworkPolicyCallback
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.server.CALLING_UID_UNMOCKED
+import com.android.server.CSAgentWrapper
+import com.android.server.CSTest
+import com.android.server.FromS
+import com.android.server.HANDLER_TIMEOUT_MS
+import com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
+import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.visibleOnHandlerThread
+import com.android.testutils.waitForIdleSerialExecutor
+import java.util.Collections
+import kotlin.test.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+
+private const val TEST_UID = 42
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSQueuedCallbacksTest(freezingBehavior: FreezingBehavior) : CSTest() {
+ companion object {
+ enum class FreezingBehavior {
+ UID_FROZEN,
+ UID_NOT_FROZEN,
+ UID_FROZEN_FEATURE_DISABLED
+ }
+
+ // Use a parameterized test with / without freezing to make it easy to compare and make sure
+ // freezing behavior (which callbacks are sent in which order) stays close to what happens
+ // without freezing.
+ @JvmStatic
+ @Parameterized.Parameters(name = "freezingBehavior={0}")
+ fun freezingBehavior() = listOf(
+ FreezingBehavior.UID_FROZEN,
+ FreezingBehavior.UID_NOT_FROZEN,
+ FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+ )
+
+ private val TAG = CSQueuedCallbacksTest::class.simpleName
+ ?: fail("Could not get test class name")
+ }
+
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ private val mockedBlockedReasonsPerUid = Collections.synchronizedMap(mutableMapOf(
+ Process.myUid() to BLOCKED_REASON_NONE,
+ TEST_UID to BLOCKED_REASON_NONE
+ ))
+
+ private val freezeUids = freezingBehavior != FreezingBehavior.UID_NOT_FROZEN
+ private val expectAllCallbacks = freezingBehavior == FreezingBehavior.UID_NOT_FROZEN ||
+ freezingBehavior == FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+ init {
+ setFeatureEnabled(
+ QUEUE_CALLBACKS_FOR_FROZEN_APPS,
+ freezingBehavior != FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+ )
+ }
+
+ @Before
+ fun subclassSetUp() {
+ // Ensure cellular stays up. CS is recreated for each test so no cleanup is necessary.
+// cm.requestNetwork(
+// NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
+// TestableNetworkCallback()
+// )
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_UpdatesAreReceived() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ val lpChangeOnConnect = agent.sendLpChange { setLinkAddresses("fe80:db8::123/64") }
+ val ncChangeOnConnect = agent.sendNcChange {
+ addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ val lpChange1WhileFrozen = agent.sendLpChange {
+ setLinkAddresses("fe80:db8::126/64")
+ }
+ val ncChange1WhileFrozen = agent.sendNcChange {
+ removeCapability(NET_CAPABILITY_NOT_ROAMING)
+ }
+ val ncChange2WhileFrozen = agent.sendNcChange {
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ }
+ val lpChange2WhileFrozen = agent.sendLpChange {
+ setLinkAddresses("fe80:db8::125/64")
+ }
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ // Verify callbacks that are sent before freezing
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ cb.expectLpWith(agent, lpChangeOnConnect)
+ cb.expectNcWith(agent, ncChangeOnConnect)
+
+ // Below callbacks should be skipped if the processes were frozen, since a single callback
+ // will be sent with the latest state after unfreezing
+ if (expectAllCallbacks) {
+ cb.expectLpWith(agent, lpChange1WhileFrozen)
+ cb.expectNcWith(agent, ncChange1WhileFrozen)
+ }
+
+ cb.expectNcWith(agent, ncChange2WhileFrozen)
+ cb.expectLpWith(agent, lpChange2WhileFrozen)
+
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_SuspendedUnsuspendedWhileFrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+ val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectNcWith(agent, rmCap)
+ cb.expect<Suspended>(agent)
+ cb.expectNcWith(agent, addCap)
+ cb.expect<Resumed>(agent)
+ } else {
+ // When frozen, a single NetworkCapabilitiesChange will be sent at unfreezing time,
+ // with nc actually identical to the original ones. This is because NetworkCapabilities
+ // callbacks were sent, but CS does not keep initial NetworkCapabilities in memory, so
+ // it cannot detect A->B->A.
+ cb.expect<CapabilitiesChanged>(agent) {
+ it.caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_UnsuspendedWhileFrozen_GetResumedCallbackWhenUnfrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ cb.expectNcWith(agent, rmCap)
+ cb.expect<Suspended>(agent)
+ cb.expectNcWith(agent, addCap)
+ cb.expect<Resumed>(agent)
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_BlockedUnblockedWhileFrozen_SingleCallbackIfFrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+ maybeSetUidsFrozen(true, TEST_UID)
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ setUidsBlockedForDataSaver(false, TEST_UID)
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ }
+ // The unblocked callback is sent in any case (with the latest blocked reason), as the
+ // blocked reason may have changed, and ConnectivityService cannot know that it is the same
+ // as the original reason as it does not keep pre-freeze blocked reasons in memory.
+ cb.expect<BlockedStatus>(agent) { !it.blocked }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_BlockedWhileFrozen_GetLastBlockedCallbackOnlyIfFrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+ maybeSetUidsFrozen(true, TEST_UID)
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ setUidsBlockedForDataSaver(false, TEST_UID)
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ cb.expect<BlockedStatus>(agent) { !it.blocked }
+ }
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkCallback_NetworkToggledWhileFrozen_NotSeen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+ wifiAgent.disconnect()
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expect<Lost>(wifiAgent)
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkCallback_NetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ waitForIdle()
+ agent.makeValidationSuccess()
+ val lpChange = agent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ val suspendedChange = agent.sendNcChange {
+ removeCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ }
+ setUidsBlockedForDataSaver(true, TEST_UID)
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ val expectLatestStatusInOnAvailable = !expectAllCallbacks
+ cb.expectAvailableCallbacks(
+ agent.network,
+ suspended = expectLatestStatusInOnAvailable,
+ validated = expectLatestStatusInOnAvailable,
+ blocked = expectLatestStatusInOnAvailable
+ )
+ if (expectAllCallbacks) {
+ cb.expectNcWith(agent) { addCapability(NET_CAPABILITY_VALIDATED) }
+ cb.expectLpWith(agent, lpChange)
+ cb.expectNcWith(agent, suspendedChange)
+ cb.expect<Suspended>(agent)
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testNetworkCallback_LocalNetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(
+ NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build(),
+ cb
+ )
+ }
+ val upstreamAgent = Agent(
+ nc = defaultNc()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET),
+ lp = defaultLp().apply { interfaceName = "wlan0" }
+ ).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ val lnc = LocalNetworkConfig.Builder().build()
+ val localAgent = Agent(
+ nc = defaultNc()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .removeCapability(NET_CAPABILITY_INTERNET),
+ lp = defaultLp().apply { interfaceName = "local42" },
+ lnc = FromS(lnc)
+ ).apply { connect() }
+ localAgent.sendLocalNetworkConfig(
+ LocalNetworkConfig.Builder()
+ .setUpstreamSelector(
+ NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ )
+ .build()
+ )
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(
+ localAgent.network,
+ validated = false,
+ upstream = if (expectAllCallbacks) null else upstreamAgent.network
+ )
+ if (expectAllCallbacks) {
+ cb.expect<LocalInfoChanged>(localAgent) {
+ it.info.upstreamNetwork == upstreamAgent.network
+ }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkRequest_NetworkSwitchesWhileFrozen_ReceiveLastNetworkUpdatesOnly() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+ }
+ val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+ val ethAgent = Agent(TRANSPORT_ETHERNET).apply { connect() }
+ waitForIdle()
+ ethAgent.makeValidationSuccess()
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expectAvailableCallbacks(ethAgent.network, validated = false)
+ cb.expectNcWith(ethAgent) { addCapability(NET_CAPABILITY_VALIDATED) }
+ } else {
+ cb.expectAvailableCallbacks(ethAgent.network, validated = true)
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkRequest_NetworkSwitchesBackWhileFrozen_ReceiveNoAvailableCallback() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+ }
+ val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+ waitForIdle()
+
+ // CS switches back to validated cell over non-validated Wi-Fi
+ cellAgent.makeValidationSuccess()
+ val cellLpChange = cellAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ // There is an extra "double validated" CapabilitiesChange callback (b/245893397), so
+ // callbacks are (AVAIL, NC, LP), extra NC, then further updates (LP and BLK here).
+ cb.expectAvailableDoubleValidatedCallbacks(cellAgent.network)
+ cb.expectLpWith(cellAgent, cellLpChange)
+ cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+ } else {
+ cb.expectNcWith(cellAgent) {
+ addCapability(NET_CAPABILITY_VALIDATED)
+ }
+ cb.expectLpWith(cellAgent, cellLpChange)
+ cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultRequestFiled_ReceiveChangeCallbacks() {
+ val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+ waitForIdle()
+
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerDefaultNetworkCallback(cb)
+ }
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ // Change LinkProperties twice before the per-app network request is applied
+ val lpChange1 = cellAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ val lpChange2 = cellAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::124/64")
+ }
+ setMobileDataPreferredUids(setOf(TEST_UID))
+
+ // Change NetworkCapabilities after the per-app network request is applied
+ val ncChange = cellAgent.sendNcChange {
+ addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ // Even if a per-app network request was filed to replace the default network request for
+ // the app, all network change callbacks are received
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectLpWith(cellAgent, lpChange1)
+ }
+ cb.expectLpWith(cellAgent, lpChange2)
+ cb.expectNcWith(cellAgent, ncChange)
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultToggled_GetStatusUpdateCallbacksOnly() {
+ // Add validated Wi-Fi and non-validated cell, expect Wi-Fi is preferred by default
+ val wifiAgent = Agent(TRANSPORT_WIFI, baseNc = makeInternetNc()).apply { connect() }
+ wifiAgent.makeValidationSuccess()
+ val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+ waitForIdle()
+
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerDefaultNetworkCallback(cb)
+ }
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ // LP change on the original Wi-Fi network
+ val lpChange = wifiAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ // Set per-app default to cell, then unset it
+ setMobileDataPreferredUids(setOf(TEST_UID))
+ setMobileDataPreferredUids(emptySet())
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(wifiAgent.network)
+ if (expectAllCallbacks) {
+ cb.expectLpWith(wifiAgent, lpChange)
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ // Cellular stops being foreground since it is now matched for this app
+ cb.expect<CapabilitiesChanged> { it.caps.hasCapability(NET_CAPABILITY_FOREGROUND) }
+ cb.expectAvailableCallbacks(wifiAgent.network)
+ } else {
+ // After switching to cell and back while frozen, only network attribute update
+ // callbacks (and not AVAILABLE) for the original Wi-Fi network should be sent
+ cb.expect<CapabilitiesChanged>(wifiAgent)
+ cb.expectLpWith(wifiAgent, lpChange)
+ cb.expect<BlockedStatus> { !it.blocked }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ private fun setUidsBlockedForDataSaver(blocked: Boolean, vararg uid: Int) {
+ val reason = if (blocked) {
+ BLOCKED_METERED_REASON_DATA_SAVER
+ } else {
+ BLOCKED_REASON_NONE
+ }
+ if (deps.isAtLeastV) {
+ visibleOnHandlerThread(csHandler) {
+ service.handleBlockedReasonsChanged(uid.map { android.util.Pair(it, reason) })
+ }
+ } else {
+ notifyLegacyBlockedReasonChanged(reason, uid)
+ waitForIdle()
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun notifyLegacyBlockedReasonChanged(reason: Int, uids: IntArray) {
+ val cbCaptor = ArgumentCaptor.forClass(NetworkPolicyCallback::class.java)
+ verify(context.networkPolicyManager).registerNetworkPolicyCallback(
+ any(),
+ cbCaptor.capture()
+ )
+ uids.forEach {
+ cbCaptor.value.onUidBlockedReasonChanged(it, reason)
+ }
+ }
+
+ private fun withCallingUid(uid: Int, action: () -> Unit) {
+ deps.callingUid = uid
+ action()
+ deps.callingUid = CALLING_UID_UNMOCKED
+ }
+
+ private fun getUidFrozenStateChangedCallback(): UidFrozenStateChangedCallback {
+ val captor = ArgumentCaptor.forClass(UidFrozenStateChangedCallback::class.java)
+ verify(activityManager).registerUidFrozenStateChangedCallback(any(), captor.capture())
+ return captor.value
+ }
+
+ private fun maybeSetUidsFrozen(frozen: Boolean, vararg uids: Int) {
+ if (!freezeUids) return
+ val state = if (frozen) UID_FROZEN_STATE_FROZEN else UID_FROZEN_STATE_UNFROZEN
+ getUidFrozenStateChangedCallback()
+ .onUidFrozenStateChanged(uids, IntArray(uids.size) { state })
+ waitForIdle()
+ }
+
+ private fun CSAgentWrapper.makeValidationSuccess() {
+ setValidationResult(
+ NETWORK_VALIDATION_RESULT_VALID,
+ probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS,
+ probesSucceeded = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS
+ )
+ cm.reportNetworkConnectivity(network, true)
+ // Ensure validation is scheduled
+ waitForIdle()
+ // Ensure validation completes on mock executor
+ waitForIdleSerialExecutor(CSTestExecutor, HANDLER_TIMEOUT_MS)
+ // Ensure validation results are processed
+ waitForIdle()
+ }
+
+ private fun setMobileDataPreferredUids(uids: Set<Int>) {
+ ConnectivitySettingsManager.setMobileDataPreferredUids(context, uids)
+ service.updateMobileDataPreferredUids()
+ waitForIdle()
+ }
+}
+
+private fun makeInternetNc() = NetworkCapabilities.Builder(defaultNc())
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+
+private fun CSAgentWrapper.sendLpChange(
+ mutator: LinkProperties.() -> Unit
+): LinkProperties.() -> Unit {
+ lp.mutator()
+ sendLinkProperties(lp)
+ return mutator
+}
+
+private fun CSAgentWrapper.sendNcChange(
+ mutator: NetworkCapabilities.() -> Unit
+): NetworkCapabilities.() -> Unit {
+ nc.mutator()
+ sendNetworkCapabilities(nc)
+ return mutator
+}
+
+private fun TestableNetworkCallback.expectLpWith(
+ agent: CSAgentWrapper,
+ change: LinkProperties.() -> Unit
+) = expect<LinkPropertiesChanged>(agent) {
+ // This test uses changes that are no-op when already applied (idempotent): verify that the
+ // change is already applied.
+ it.lp == LinkProperties(it.lp).apply(change)
+}
+
+private fun TestableNetworkCallback.expectNcWith(
+ agent: CSAgentWrapper,
+ change: NetworkCapabilities.() -> Unit
+) = expect<CapabilitiesChanged>(agent) {
+ it.caps == NetworkCapabilities(it.caps).apply(change)
+}
+
+private fun LinkProperties.setLinkAddresses(vararg addrs: String) {
+ setLinkAddresses(addrs.map { LinkAddress(it) })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 88c2738..5ca7fcc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -54,6 +54,7 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -162,6 +163,43 @@
doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
}
+ private fun doTestUnregisterAfterReplacementSatisfier(destroyed: Boolean) {
+ val satelliteAgent = createSatelliteAgent("satellite0")
+ satelliteAgent.connect()
+
+ val uids = setOf(TEST_PACKAGE_UID)
+ updateSatelliteNetworkFallbackUids(uids)
+
+ if (destroyed) {
+ satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+ }
+
+ updateSatelliteNetworkFallbackUids(setOf())
+ if (destroyed) {
+ // If the network is already destroyed, networkRemoveUidRangesParcel should not be
+ // called.
+ verify(netd, never()).networkRemoveUidRangesParcel(any())
+ } else {
+ verify(netd).networkRemoveUidRangesParcel(
+ NativeUidRangeConfig(
+ satelliteAgent.network.netId,
+ toUidRangeStableParcels(uidRangesForUids(uids)),
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_destroyed() {
+ doTestUnregisterAfterReplacementSatisfier(destroyed = true)
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
+ doTestUnregisterAfterReplacementSatisfier(destroyed = false)
+ }
+
private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
val nris: Set<ConnectivityService.NetworkRequestInfo> =
service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
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 13c5cbc..1f5ee32 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -192,7 +192,8 @@
connect()
}
- fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+ fun setValidationResult(result: Int, probesCompleted: Int, probesSucceeded: Int) {
+ nmValidationResult = result
nmProbesCompleted = probesCompleted
nmProbesSucceeded = probesSucceeded
}
@@ -204,8 +205,10 @@
// 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 */)
+ setValidationResult(
+ VALIDATION_RESULT_INVALID,
+ probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP,
+ probesSucceeded = NO_PROBE_RESULT
+ )
}
}
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 ed72fd2..46c25d2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -165,6 +165,7 @@
it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
+ it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
}
fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
@@ -537,8 +538,12 @@
provider: NetworkProvider? = null
) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
nac, nc, lp, lnc, score, provider)
- fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
- val nc = NetworkCapabilities.Builder().apply {
+ fun Agent(
+ vararg transports: Int,
+ baseNc: NetworkCapabilities = defaultNc(),
+ lp: LinkProperties = defaultLp()
+ ): CSAgentWrapper {
+ val nc = NetworkCapabilities.Builder(baseNc).apply {
transports.forEach {
addTransportType(it)
}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
new file mode 100644
index 0000000..c8b2f65
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
@@ -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.
+ */
+// ktlint does not allow annotating function argument literals inline. Disable the specific rule
+// since this negatively affects readability.
+@file:Suppress("ktlint:standard:comment-wrapping")
+
+package com.android.server.ethernet
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.os.Build
+import android.os.Handler
+import android.os.test.TestLooper
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val IFACE = "eth0"
+private val CAPS = NetworkCapabilities.Builder().build()
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class EthernetInterfaceStateMachineTest {
+ private lateinit var looper: TestLooper
+ private lateinit var handler: Handler
+ private lateinit var ifaceState: EthernetInterfaceStateMachine
+
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var provider: NetworkProvider
+ @Mock private lateinit var deps: EthernetNetworkFactory.Dependencies
+
+ // There seems to be no (obvious) way to force execution of @Before and @Test annotation on the
+ // same thread. Since SyncStateMachine requires all interactions to be called from the same
+ // thread that is provided at construction time (in this case, the thread that TestLooper() is
+ // called on), setUp() must be called directly from the @Test method.
+ // TODO: find a way to fix this in the test runner.
+ fun setUp() {
+ looper = TestLooper()
+ handler = Handler(looper.looper)
+ MockitoAnnotations.initMocks(this)
+
+ ifaceState = EthernetInterfaceStateMachine(IFACE, handler, context, CAPS, provider, deps)
+ }
+
+ @Test
+ fun testUpdateLinkState_networkOfferRegisteredAndRetracted() {
+ setUp()
+
+ ifaceState.updateLinkState(/* up= */ true)
+
+ // link comes up: validate the NetworkOffer is registered and capture callback object.
+ val inOrder = inOrder(provider)
+ val networkOfferCb = ArgumentCaptor.forClass(NetworkOfferCallback::class.java).also {
+ inOrder.verify(provider).registerNetworkOffer(any(), any(), any(), it.capture())
+ }.value
+
+ ifaceState.updateLinkState(/* up */ false)
+
+ // link goes down: validate the NetworkOffer is retracted
+ inOrder.verify(provider).unregisterNetworkOffer(eq(networkOfferCb))
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7e0a225..ef4c44d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -56,6 +56,7 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.net.TrafficStats.UID_REMOVED;
import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.TrafficStats.getValueForTypeFromFirstEntry;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
@@ -69,19 +70,22 @@
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.ACTION_NETWORK_STATS_UPDATED;
+import static com.android.server.net.NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG;
import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
-import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES;
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;
-import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG;
+import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
@@ -101,8 +105,10 @@
import android.annotation.NonNull;
import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -138,6 +144,7 @@
import android.provider.Settings;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
@@ -150,6 +157,7 @@
import com.android.connectivity.resources.R;
import com.android.internal.util.FileRotator;
import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
@@ -613,8 +621,15 @@
}
@Override
- public boolean alwaysUseTrafficStatsRateLimitCache(Context ctx) {
- return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+ public boolean alwaysUseTrafficStatsServiceRateLimitCache(Context ctx) {
+ return mFeatureFlags.getOrDefault(
+ TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+ }
+
+ @Override
+ public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+ return mFeatureFlags.getOrDefault(
+ BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, true);
}
@Override
@@ -623,8 +638,8 @@
}
@Override
- public int getTrafficStatsRateLimitCacheMaxEntries() {
- return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+ public int getTrafficStatsServiceRateLimitCacheMaxEntries() {
+ return DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES;
}
@Override
@@ -2438,28 +2453,28 @@
assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
@Test
public void testTrafficStatsRateLimitCache_disabledWithCompatChangeEnabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
doTestTrafficStatsRateLimitCache(true /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
@Test
public void testTrafficStatsRateLimitCache_enabledWithCompatChangeEnabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
doTestTrafficStatsRateLimitCache(true /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
@Test
public void testTrafficStatsRateLimitCache_disabledWithCompatChangeDisabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
doTestTrafficStatsRateLimitCache(false /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
@Test
public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
@@ -2501,11 +2516,13 @@
private void assertTrafficStatsValues(String iface, int uid, long rxBytes, long rxPackets,
long txBytes, long txPackets) {
assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getTotalStats(type));
+ (type) -> getValueForTypeFromFirstEntry(mService.getTypelessTotalStats(), type));
assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getIfaceStats(iface, type));
+ (type) -> getValueForTypeFromFirstEntry(
+ mService.getTypelessIfaceStats(iface), type)
+ );
assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getUidStats(uid, type));
+ (type) -> getValueForTypeFromFirstEntry(mService.getTypelessUidStats(uid), type));
}
private void assertTrafficStatsValuesThat(long rxBytes, long rxPackets, long txBytes,
@@ -2617,6 +2634,8 @@
private void mockDefaultSettings() throws Exception {
mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+ mSettings.setBroadcastNetworkStatsUpdateDelayMs(
+ NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS);
}
private void mockSettings(long bucketDuration, long deleteAge) {
@@ -2631,6 +2650,8 @@
@NonNull
private volatile Config mConfig;
private final AtomicBoolean mCombineSubtypeEnabled = new AtomicBoolean();
+ private long mBroadcastNetworkStatsUpdateDelayMs =
+ NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
TestNetworkStatsSettings(long bucketDuration, long deleteAge) {
mConfig = new Config(bucketDuration, deleteAge, deleteAge);
@@ -2693,6 +2714,15 @@
public boolean getAugmentEnabled() {
return false;
}
+
+ @Override
+ public long getBroadcastNetworkStatsUpdateDelayMs() {
+ return mBroadcastNetworkStatsUpdateDelayMs;
+ }
+
+ public void setBroadcastNetworkStatsUpdateDelayMs(long broadcastDelay) {
+ mBroadcastNetworkStatsUpdateDelayMs = broadcastDelay;
+ }
}
private void assertStatsFilesExist(boolean exist) {
@@ -3064,4 +3094,91 @@
final String dump = getDump();
assertDumpContains(dump, "Log for testing");
}
+
+ private static class TestNetworkStatsUpdatedReceiver extends BroadcastReceiver {
+ private final ArrayTrackRecord<Intent>.ReadHead mHistory;
+
+ TestNetworkStatsUpdatedReceiver() {
+ mHistory = (new ArrayTrackRecord<Intent>()).newReadHead();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHistory.add(intent);
+ }
+
+ /**
+ * Assert no broadcast intent is received in blocking manner
+ */
+ public void assertNoBroadcastIntentReceived() {
+ assertNull(mHistory.peek());
+ }
+
+ /**
+ * Assert an intent is received and remove it from queue
+ */
+ public void assertBroadcastIntentReceived() {
+ assertNotNull(mHistory.poll(WAIT_TIMEOUT, number -> true));
+ }
+ }
+
+ @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG)
+ @Test
+ public void testNetworkStatsUpdatedIntentSpam_rateLimitOn() throws Exception {
+ // Set the update delay long enough that messages won't be processed before unblocked
+ // Set a short time to test the behavior before reaching delay.
+ // Constraint: test running time < toleranceMs < update delay time
+ mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+ final long toleranceMs = 5000;
+
+ final TestableLooper mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+ final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+ mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+ try {
+ // Test that before anything, the intent is delivered immediately
+ mService.forceUpdate();
+ mTestableLooper.processAllMessages();
+ receiver.assertBroadcastIntentReceived();
+ receiver.assertNoBroadcastIntentReceived();
+
+ // Test that the next two intents results in exactly one intent delivered
+ for (int i = 0; i < 2; i++) {
+ mService.forceUpdate();
+ }
+ // Test that the delay depends on our set value
+ mTestableLooper.moveTimeForward(mSettings.getBroadcastNetworkStatsUpdateDelayMs()
+ - toleranceMs);
+ mTestableLooper.processAllMessages();
+ receiver.assertNoBroadcastIntentReceived();
+
+ // Unblock messages and test that the second and third update
+ // is broadcasted right after the delay
+ mTestableLooper.moveTimeForward(toleranceMs);
+ mTestableLooper.processAllMessages();
+ receiver.assertBroadcastIntentReceived();
+ receiver.assertNoBroadcastIntentReceived();
+
+ } finally {
+ mTestableLooper.destroy();
+ }
+ }
+
+ @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, enabled = false)
+ @Test
+ public void testNetworkStatsUpdatedIntentSpam_rateLimitOff() throws Exception {
+ // Set the update delay long enough to ensure that messages are processed
+ // despite the rate limit.
+ mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+
+ final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+ mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+ for (int i = 0; i < 2; i++) {
+ mService.forceUpdate();
+ waitForIdle();
+ receiver.assertBroadcastIntentReceived();
+ }
+ receiver.assertNoBroadcastIntentReceived();
+ }
}
diff --git a/thread/README.md b/thread/README.md
index 41b73ac..f2bd3b2 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -16,3 +16,7 @@
Open `https://localhost:8443/` in your web browser, you can find the Thread
demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
+
+## More docs
+
+- [Make your Android Border Router](./docs/make-your-android-border-router.md)
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index edf000a..838c0d9 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -23,8 +23,8 @@
// See https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
// for details of versioned rc files.
prebuilt_etc {
- name: "ot-daemon.init.34rc",
+ name: "ot-daemon.34rc",
src: "ot-daemon.34rc",
- filename: "init.34rc",
+ filename: "ot-daemon.34rc",
installable: false,
}
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index 117b4f9..a786639 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -32,7 +32,7 @@
"guava",
],
libs: [
- "framework-connectivity-t",
+ "framework-connectivity-t.stubs.module_lib",
],
required: [
"privapp-permissions-com.android.threadnetwork.demoapp",
diff --git a/thread/docs/android-thread-arch.png b/thread/docs/android-thread-arch.png
new file mode 100644
index 0000000..ea408fa
--- /dev/null
+++ b/thread/docs/android-thread-arch.png
Binary files differ
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
new file mode 100644
index 0000000..f90a23b
--- /dev/null
+++ b/thread/docs/build-an-android-border-router.md
@@ -0,0 +1,526 @@
+# Build an Android Border Router
+
+If you are not an Android device or Thread chip vendor, you can stop reading
+now.
+
+This document walks you through the steps to build a new Android-based Thread
+Border Router device with the latest AOSP source code. By following this
+document, you will learn:
+
+1. [the overall architecture and status of Thread support in Android](#architecture)
+2. [how to create your own Thread HAL service](#build-your-thread-hal-service)
+3. [how to make your device compatible with Google Home](#be-compatible-with-google-home)
+4. [how to test your Thread Border Router](#testing)
+
+If you need support, file an issue in
+[GitHub](https://github.com/openthread/ot-br-posix/issues) or open a
+[Dicussion](https://github.com/orgs/openthread/discussions) if you have any
+questions.
+
+Note: Before creating an issue or discussion, search to see if it has already
+been reported or asked.
+
+## Overview
+
+The Android Thread stack is based on OpenThread and `ot-br-posix` which are
+open-sourced by Google in GitHub. The same way OpenThread is developed in a
+public GitHub repository, so the Android Thread stack is developed in the
+public [AOSP codebase](https://cs.android.com/). All features and bug fixes
+are submitted first in AOSP. This allows vendors to start adopting the latest
+Thread versions without waiting for regular Android releases.
+
+### Architecture
+
+The whole Android Thread stack consists of two major components: the core
+Thread stack in a generic system partition and the Thread HAL service in a
+vendor partition. Device vendors typically need only to take care and build the
+HAL service.
+
+
+
+Here is a brief summary of how the Android Thread stack works:
+- There is a Java Thread system service in the system server which manages the
+ whole stack - provides the Thread system API, creates the `thread-wpan`
+ tunnel interface, registers the Thread network to the
+ [Connectivity service](https://developer.android.com/reference/android/content/Context#CONNECTIVITY_SERVICE)
+ and implements the Border Routing and Advertising Proxy functionalities.
+- The core Thread / OpenThread stack is hosted in a non-privileged standalone
+ native process which is named `ot-daemon`. `ot-daemon` is directly managed
+ by the Java system service via private AIDL APIs and it accesses the Thread
+ hardware radio through the Thread HAL API.
+- A vendor-provided Thread HAL service MUST implement the Thread HAL API. It
+ typically works as an
+ [RCP](https://openthread.io/platforms/co-processor#radio_co-processor_rcp)
+ and implements the
+ [spinel](https://openthread.io/platforms/co-processor#spinel_protocol)
+ protocol.
+
+Note: Both the Java system service and ot-daemon are delivered in a
+[Tethering](https://source.android.com/docs/core/ota/modular-system/tethering#overview)
+mainline module. For mobile devices, the binary is managed by Google and
+delivered to devices via Google Play monthly. For non-mobile devices such as
+TV, vendors are free to build from source or use a prebuilt.
+
+### Where is the code?
+
+- The Android Thread framework / API and service: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/thread/
+- The Thread HAL API and default service implementation: https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/
+- Imported OpenThread repo: https://cs.android.com/android/platform/superproject/main/+/main:external/openthread/
+- Imported ot-br-posix repo: https://cs.android.com/android/platform/superproject/main/+/main:external/ot-br-posix/
+
+## Set up development environment
+
+Android device vendors who have already established an Android development
+environment for the device can skip this section.
+
+If you are new to Android ecosystem or you are a silicon vendor who wants to
+make your Thread chip compatible with Android and provide support for device
+vendors, keep reading.
+
+### Follow the Android developer codelab
+
+To set up your Android development environment for the first time, use the
+following codelab: https://source.android.com/docs/setup/start. At the end of
+this codelab, you will be able to build and run a simulated Cuttlefish device
+from source code.
+
+## Build your Thread HAL service
+
+### Try Thread in Cuttlefish
+
+[Cuttlefish](https://source.android.com/docs/devices/Cuttlefish) is the virtual
+Android device. Before starting building your own HAL service, it's better to
+try Thread in Cuttlefish to understand how HAL works.
+
+A default Thread HAL service is provided in Cuttlefish and it's implemented
+with the [simulated RCP](https://github.com/openthread/openthread/tree/main/examples/platforms/simulation)
+which transceives packets via UDP socket to and from a simulated
+Thread (802.15.4) radio.
+
+In the Cuttlefish instance, a "ThreadNetworkDemoApp" is pre-installed. Open
+that app to join the Cuttlefish device into a default Thread network.
+
+
+
+Note: The Border Router functionalities will be started (and OMR address will
+be created) only when the Cuttlefish device is connected to a virtual Wi-Fi
+network. You can connect the Wi-Fi network in Settings.
+
+There are also the `ot-ctl` and `ot-cli-ftd` command line tools provided to
+configure your Thread network for testing. Those tools support all the
+OpenThread CLI commands that you may be familiar with already.
+
+You can grep for logs of the Cuttlefish Thread HAL service by:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+
+07-21 10:43:05.048 0 0 I init : Parsing file /apex/com.android.hardware.threadnetwork/etc/threadnetwork-service.rc...
+07-21 10:59:27.233 580 580 W android.hardware.threadnetwork-service: ThreadChip binder is unlinked
+07-21 10:59:27.233 580 580 I android.hardware.threadnetwork-service: Close IThreadChip successfully
+07-21 10:59:27.385 580 580 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+Or grep for ot-daemon logs by:
+
+```
+$ adb logcat | egrep -i ot-daemon
+07-21 10:43:48.741 0 0 I init : starting service 'ot-daemon'...
+07-21 10:43:48.742 0 0 I init : Created socket '/dev/socket/ot-daemon/thread-wpan.sock', mode 660, user 1084, group 1084
+07-21 10:43:48.762 0 0 I init : ... started service 'ot-daemon' has pid 2473
+07-21 10:46:26.320 2473 2473 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-21 10:46:30.290 2473 2473 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-21 10:48:07.264 2473 2473 I ot-daemon: [INFO]-BINDER--: Start joining...
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] Settings------: Saved ActiveDataset
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] DatasetManager: Active dataset set
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] DnssdServer---: Started
+07-21 10:48:07.273 2473 2473 I ot-daemon: [N] Mle-----------: Role disabled -> detached
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Mle-----------: AttachState Idle -> Start
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) [Ip6+ Role LLAddr MLAddr KeySeqCntr Ip6Mult+ Channel PanId NetName ExtPanId ...
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) ... NetworkKey PSKc SecPolicy NetifState ActDset]
+```
+
+Note: You can also capture Thread system server log with the tag
+"ThreadNetworkService".
+
+The Cuttlefish Thread HAL service uses the default Thread HAL service plus the
+OpenThread simulated RCP binary, see the next section for how it works.
+
+### The default HAL service
+
+A [default HAL service](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/aidl/default/)
+is included along with the Thread HAL API. The default HAL service supports
+both simulated and real RCP devices. It receives an optional RCP device URL and
+if the URL is not provided, it defaults to the simulated RCP device.
+
+In file `hardware/interfaces/threadnetwork/aidl/default/threadnetwork-service.rc`:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service
+ class hal
+ user thread_network
+```
+
+This is equivalent to:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+forkpty:///apex/com.android.hardware.threadnetwork/bin/ot-rcp?forkpty-arg=1
+ class hal
+ user thread_network
+```
+
+For real RCP devices, it supports both SPI and UART interfaces and you can
+specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
+`spinel+socket://` respectively.
+
+Note: `spinel+socket://` is a new spinel interface added in the default Thread
+HAL, it supports transmitting spinel frame via an Unix socket. A full
+socket-based radio URL may be like
+`spinel+socket:///data/vendor/threadnetwork/thread_spinel_socket`.
+
+#### Understand the vendor APEX
+
+Similar to the Thread stack in the Tethering mainline module, the default Thread
+HAL service in Cuttlefish is packaged in an APEX module as well. But it's a
+vendor APEX module which will be installed to `/vendor/apex/` (The artifacts in
+the module will be unzipped to `/apex/com.android.hardware.threadnetwork/`).
+
+```aidl
+apex {
+ name: "com.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ "ot-rcp",
+ ],
+
+ prebuilts: [
+ "threadnetwork-default.xml", // vintf_fragment
+ "threadnetwork-service.rc", // init_rc
+ "android.hardware.thread_network.prebuilt.xml", // permission
+ ],
+}
+```
+
+There are a few important configurations that you will need to pay attention or
+make changes to when building your own HAL APEX module:
+
+- `file_contexts`: This describes the binary / data files delivered in this
+ APEX module or files the HAL service need to access (for example, the RCP
+ device). This allows you to specify specific sepolicy rules for your HAL
+ service to access the hardware RCP device.
+- `binaries`: The binary file delivered in this APEX module
+- `threadnetwork-service.rc`: How the HAL service will be started. You need to
+ specify the RCP device path here.
+- `android.hardware.thread_network.prebuilt.xml`: Defines the
+ `android.hardware.thread_network` hardware feature. This is required for the
+ Android system to know that your device does have Thread hardware support.
+ Otherwise, the Android Thread stack won't be enabled.
+
+### Create your HAL service
+
+Whether you are an Android device developer or a silicon vendor, you should be
+familiar with building OT RCP firmware for your Thread chip. The following
+instructions assume that the hardware chip is correctly wired and
+validated.
+
+The simplest way to build your HAL APEX is to create a new APEX with the
+binaries and prebuilts of the default HAL APEX. For example, if your company is
+Banana and the RCP device on your device is `/dev/ttyACM0`, your Thread HAL
+APEX will look like this:
+
+- `Android.bp`:
+ ```
+ prebuilt_etc {
+ name: "banana-threadnetwork-service.rc",
+ src: "banana-threadnetwork-service.rc",
+ installable: false,
+ }
+
+ apex {
+ name: "com.banana.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ ],
+
+ prebuilts: [
+ "banana-threadnetwork-service.rc",
+ "threadnetwork-default.xml",
+ "android.hardware.thread_network.prebuilt.xml",
+ ],
+ }
+ ```
+- `file_contexts`:
+ ```
+ (/.*)? u:object_r:vendor_file:s0
+ /etc(/.*)? u:object_r:vendor_configs_file:s0
+ /bin/hw/android\.hardware\.threadnetwork-service u:object_r:hal_threadnetwork_default_exec:s0
+ /dev/ttyACM0 u:object_r:threadnetwork_rcp_device:s0
+ ```
+ The file paths in the first column are related to `/apex/com.android.hardware.threadnetwork/`.
+- `threadnetwork-service.rc`:
+ ```
+ service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=115200
+ class hal
+ user root
+ ```
+- `manifest.json`:
+ ```
+ {
+ "name": "com.android.hardware.threadnetwork",
+ "version": 1
+ }
+ ```
+
+Note: The default Thread HAL service is not designed to be a generic system
+component which works for all Android devices. If the default implementation
+can't support your device, you are free to make a copy and change it for your
+needs. In this case, it's just simpler to create a new APEX module without
+overriding the default one.
+
+Assuming you are making a new device named Orange, your device specific
+configuration directory will be like:
+
+```
+device/banana/orange/threadnetwork/
+ sepolicy/
+ Android.bp
+ file_contexts
+ manifest.json
+ threadnetwork-default.xml
+ threadnetwork-service.rc
+```
+
+See the next section for what sepolicy rules should be added in the `sepolicy/`
+sub-directory.
+
+#### Sepolicy rules for RCP device
+
+By default, your Thread HAL service doesn't have access to the RCP device (for
+example `/dev/ttyACM0`), custom sepolicy rules need to be added to the
+`sepolicy/` directory.
+
+Create a new `sepolicy/threadnetwork_hal.te` file with below content:
+
+```
+type threadnetwork_rcp_device, dev_type;
+
+# Allows the Thread HAL service to read / write the Thread RCP device
+allow hal_threadnetwork_default threadnetwork_rcp_device:chr_file rw_file_perms;
+```
+
+#### Put together
+
+Now you have finished almost all the code needs for adding Thread, the last
+step is to add the Thread HAL APEX and sepolicy rules to your device's image.
+
+You can do this by adding below code to your device's `Makefile` (for example,
+`device.mk`):
+
+```
+PRODUCT_PACKAGES += com.banana.hardware.threadnetwork
+BOARD_SEPOLICY_DIRS += device/banana/orange/threadnetwork/sepolicy
+```
+
+Note: Unfortunately, APEX module doesn't support sepolicy rules, so you need
+to explicitly specify the sepolicy directory separately.
+
+If everything works, now you will be able to see the Thread HAL service log similar to:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: ServiceName: android.hardware.threadnetwork.IThreadChip/chip0, Url: spinel+spi
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: Thread Network HAL is running
+08-13 13:26:55.165 477 477 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+And the `ot-daemon` log will be like:
+
+```
+$ adb logcat -s ot-daemon
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Running OTBR_AGENT/Unknown
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread version: 1.3.0
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread interface: thread-wpan
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Backbone interface is not specified
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Radio URL: threadnetwork_hal://binder?none
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-ILS-----: Infra link selected:
+08-13 13:26:55.160 1019 1019 I ot-daemon: [I] Platform------: [HAL] Wait for getting the service android.hardware.threadnetwork.IThreadChip/chip0 ...
+08-13 13:26:55.165 1019 1019 I ot-daemon: [I] Platform------: [HAL] Successfully got the service android.hardware.threadnetwork.IThreadChip/chip0
+08-13 13:26:55.275 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_UNKNOWN
+08-13 13:26:55.276 1019 1019 I ot-daemon: [I] P-RadioSpinel-: Software reset RCP successfully
+08-13 13:26:55.277 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_POWER_ON
+08-13 13:26:55.322 1019 1019 I ot-daemon: [I] ChildSupervsn-: Timeout: 0 -> 190
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Initializing - InfraIfIndex:0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] InfraIf-------: Init infra netif 0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] Settings------: Read BrUlaPrefix fd7b:cc45:ff06::/48
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: BR ULA prefix: fd7b:cc45:ff06::/48 (loaded)
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Generated local OMR prefix: fd7b:cc45:ff06:1::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: Local on-link prefix: fdde:ad00:beef:cafe::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Enabling
+```
+
+## Customization
+
+The Thread mainline module (it's actually a part of the "Tethering" module)
+provides a few [overlayable configurations](https://source.android.com/docs/core/runtime/rros)
+which can be specified by vendors to customize the stack behavior. See
+[config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
+for the full list.
+
+Typically, you must change the `config_thread_vendor_name`,
+`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
+product values. Those values will be included in the `_meshcop._udp` mDNS
+service which is always advertised by a Thread Border Router.
+
+To add the overlay, you need to create a new `ConnectivityOverlayOrange`
+runtime_resource_overlay target for your Orange device. Create a new
+`ConnectivityOverlay/` directory under `device/banana/orange/rro_overlays` and
+create below contents in it:
+
+```
+device/banana/orange/rro_overlays/ConnectivityOverlay/
+ res
+ values
+ config_thread.xml
+ Android.bp
+ AndroidManifest.xml
+```
+
+- `Android.bp`:
+ ```
+ package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ }
+
+ runtime_resource_overlay {
+ name: "ConnectivityOverlayOrange",
+ manifest: "AndroidManifest.xml",
+ resource_dirs: ["res"],
+ certificate: "platform",
+ product_specific: true,
+ sdk_version: "current",
+ }
+ ```
+- `AndroidManifest.xml`:
+ ```
+ <!-- Orange overlays for the Connectivity module -->
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.banana.android.connectivity.resources.orange"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:hasCode="false" />
+
+ <!-- If your device uses google-signed mainline modules, the targetPackage
+ needs to be "com.google.android.connectivity.resources", otherise, it
+ should be "com.android.connectivity.resources"
+ -->
+ <overlay
+ android:targetPackage="com.google.android.connectivity.resources"
+ android:targetName="ServiceConnectivityResourcesConfig"
+ android:isStatic="true"
+ android:priority="1"/>
+ </manifest>
+ ```
+- `config_thread.xml`:
+ ```
+ <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
+ <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
+ <string translatable="false" name="config_thread_model_name">Orange</string>
+ ```
+
+Similar to the HAL APEX, you need to add the overlay app to your `device.mk`
+file:
+
+```
+PRODUCT_PACKAGES += \
+ ConnectivityOverlayOrange
+```
+
+If everything works, you will see that `ot-daemon` logs the vendor and model name
+at the very beginning of the log:
+```
+$ adb logcat -s ot-daemon
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Input: state
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: disabled
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: Done
+07-22 15:31:37.693 1472 1472 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] Cli-----------: Input: factoryreset
+07-22 15:31:50.092 1472 1472 I ot-daemon: [I] Settings------: Wiped all info
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-ADPROXY-: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-DPROXY--: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Stop Thread Border Agent
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Unpublish meshcop service Banana Inc. Orange #4833._meshcop._udp.local
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Removing service Banana Inc. Orange #4833._meshcop._udp
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Unpublishing service Banana Inc. Orange #4833._meshcop._udp listener ID = 0
+```
+
+Note: In case the overlay doesn't work, check https://source.android.com/docs/core/runtime/rro-troubleshoot
+for troubleshooting instructions.
+
+### Be compatible with Google Home
+
+Additionally, if you want to make your Border Router be used by the Google Home
+ecosystem, you can specify this configuration in `config_thread.xml`:
+
+```
+<string-array name="config_thread_mdns_vendor_specific_txts">
+ <item>vgh=1</item>
+</string-array>
+```
+
+## Testing
+
+Your device should be compatible with the Thread 1.3+ Border Router
+specification now. Before sending it to the Thread certification program, there
+are a few Android xTS tests should be exercised to ensure the compatibility.
+
+- The VTS test makes sure Thread HAL service work as expected on your device.
+ You can run the tests with command
+ ```
+ atest VtsHalThreadNetworkTargetTest
+ ```
+- The CTS test makes sure Thread APIs work as expected on your device. You can
+ run the tests with command
+ ```
+ atest CtsThreadNetworkTestCases
+ ```
+- The integration test provides more quality guarantee of how the Thread
+ mainline code works on your device. You can run the tests with command
+ ```
+ atest ThreadNetworkIntegrationTests
+ ```
+
+You can also find more instructions of how to run VTS/CTS/MTS tests with those
+released test suites:
+
+- https://source.android.com/docs/core/tests/vts
+- https://source.android.com/docs/compatibility/cts/run
+- https://docs.partner.android.com/mainline/test/mts (you need to be a partner to access this link)
+
+### Test with the Thread demo app
+
+Similar to the Cuttlefish device, you can add the Thread demo app to your system image:
+
+```
+# ThreadNetworkDemoApp for testing
+PRODUCT_PACKAGES_DEBUG += ThreadNetworkDemoApp
+```
+
+Note that you should add it to only the debug / eng variant (for example,
+`PRODUCT_PACKAGES_DEBUG`) given this is not supposed to be included in user
+build for end consumers.
diff --git a/thread/docs/demoapp-screenshot.png b/thread/docs/demoapp-screenshot.png
new file mode 100644
index 0000000..fa7f079
--- /dev/null
+++ b/thread/docs/demoapp-screenshot.png
Binary files differ
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index 22457f5..1b50ba7 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -35,6 +35,7 @@
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
import java.io.ByteArrayOutputStream;
import java.net.Inet6Address;
@@ -69,7 +70,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ActiveOperationalDataset implements Parcelable {
/** The maximum length of the Active Operational Dataset TLV array in bytes. */
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
index bcda8a8..abc00b9 100644
--- a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -16,11 +16,11 @@
package android.net.thread;
- /**
- * Mapping from a channel to its max power.
- *
- * {@hide}
- */
+/**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
parcelable ChannelMaxPower {
int channel; // The Thread radio channel.
int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
similarity index 64%
copy from tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
copy to thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
index f343df5..dcc4545 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
+++ b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 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.
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package com.android.cts.netpolicy.hostside;
+package android.net.thread;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
+import android.net.thread.ThreadConfiguration;
-@RequiredProperties({NON_METERED_NETWORK})
-public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+/** Receives the result of a Thread Configuration change. @hide */
+oneway interface IConfigurationReceiver {
+ void onConfigurationChanged(in ThreadConfiguration configuration);
}
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
index b576b33..3fece65 100644
--- a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -23,6 +23,8 @@
* @hide
*/
oneway interface IOperationalDatasetCallback {
- void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
- void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+ void onActiveOperationalDatasetChanged(
+ in @nullable ActiveOperationalDataset activeOpDataset);
+ void onPendingOperationalDatasetChanged(
+ in @nullable PendingOperationalDataset pendingOpDataset);
}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java b/thread/framework/java/android/net/thread/IOutputReceiver.aidl
similarity index 65%
rename from tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
rename to thread/framework/java/android/net/thread/IOutputReceiver.aidl
index f343df5..b6b4375 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
+++ b/thread/framework/java/android/net/thread/IOutputReceiver.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 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.
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package com.android.cts.netpolicy.hostside;
+package android.net.thread;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+/** Receives the output of a Thread network operation. @hide */
+oneway interface IOutputReceiver {
+ void onOutput(in String output);
+ void onComplete();
+ void onError(int errorCode, String errorMessage);
}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index 9d0a571..57c365b 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -23,4 +23,6 @@
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
void onThreadEnableStateChanged(int enabledState);
+ void onEphemeralKeyStateChanged(
+ int ephemeralKeyState, @nullable String ephemeralKey, long expiryMillis);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index c5ca557..e9cbb83 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -19,16 +19,18 @@
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IScheduleMigrationReceiver;
import android.net.thread.IStateCallback;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
/**
-* Interface for communicating with ThreadNetworkControllerService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkControllerService.
+ * @hide
+ */
interface IThreadNetworkController {
void registerStateCallback(in IStateCallback callback);
void unregisterStateCallback(in IStateCallback callback);
@@ -36,14 +38,22 @@
void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
- void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+ void scheduleMigration(
+ in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
- void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+ void setTestNetworkAsUpstream(
+ in String testNetworkInterfaceName, in IOperationReceiver receiver);
void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
void setEnabled(boolean enabled, in IOperationReceiver receiver);
+ void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
+ void registerConfigurationCallback(in IConfigurationReceiver receiver);
+ void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
+
+ void activateEphemeralKeyMode(long lifetimeMillis, in IOperationReceiver receiver);
+ void deactivateEphemeralKeyMode(in IOperationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
index 0e394b1..b63cd72 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -19,9 +19,9 @@
import android.net.thread.IThreadNetworkController;
/**
-* Interface for communicating with ThreadNetworkService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkService.
+ * @hide
+ */
interface IThreadNetworkManager {
List<IThreadNetworkController> getAllThreadNetworkControllers();
}
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index 520acbd..489f941 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -26,6 +26,8 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import com.android.net.thread.flags.Flags;
+
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Objects;
@@ -37,7 +39,7 @@
* @see PendingOperationalDataset
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class OperationalDatasetTimestamp {
/** @hide */
@@ -65,11 +67,32 @@
*/
@NonNull
public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
+ return OperationalDatasetTimestamp.fromInstant(instant, true /* isAuthoritativeSource */);
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
+ *
+ * <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
+ * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
+ * is set to {@code isAuthoritativeSource}.
+ *
+ * <p>Note that this conversion can lose precision and a value returned by {@link #toInstant}
+ * may not equal exactly the {@code instant}.
+ *
+ * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+ * 0xffffffffffffL}
+ * @see toInstant
+ * @hide
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromInstant(
+ @NonNull Instant instant, boolean isAuthoritativeSource) {
int ticks = getRoundedTicks(instant.getNano());
long seconds = instant.getEpochSecond() + ticks / TICKS_UPPER_BOUND;
// the rounded ticks can be 0x8000 if instant.getNano() >= 999984742
ticks = ticks % TICKS_UPPER_BOUND;
- return new OperationalDatasetTimestamp(seconds, ticks, true /* isAuthoritativeSource */);
+ return new OperationalDatasetTimestamp(seconds, ticks, isAuthoritativeSource);
}
/**
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index c1351af..235e563 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -27,6 +27,8 @@
import android.os.Parcelable;
import android.util.SparseArray;
+import com.android.net.thread.flags.Flags;
+
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
@@ -42,7 +44,7 @@
* @see ThreadNetworkController#scheduleMigration
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class PendingOperationalDataset implements Parcelable {
// Value defined in Thread spec 8.10.1.16
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
similarity index 68%
rename from tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
rename to thread/framework/java/android/net/thread/ThreadConfiguration.aidl
index 7aac2ab..9473411 100644
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright 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.
@@ -14,13 +14,6 @@
* limitations under the License.
*/
-package com.android.cts.netpolicy.hostside;
+package android.net.thread;
-import android.net.NetworkInfo;
-
-@JavaDerive(toString=true)
-parcelable NetworkCheckResult {
- boolean connected;
- String details;
- NetworkInfo networkInfo;
-}
\ No newline at end of file
+parcelable ThreadConfiguration;
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
new file mode 100644
index 0000000..1c25535
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 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 android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.thread.flags.Flags;
+
+import java.util.Objects;
+
+/**
+ * Data interface for Thread device configuration.
+ *
+ * <p>An example usage of creating a {@link ThreadConfiguration} that turns on NAT64 feature based
+ * on an existing {@link ThreadConfiguration}:
+ *
+ * <pre>{@code
+ * ThreadConfiguration config =
+ * new ThreadConfiguration.Builder(existingConfig).setNat64Enabled(true).build();
+ * }</pre>
+ *
+ * @see ThreadNetworkController#setConfiguration
+ * @see ThreadNetworkController#registerConfigurationCallback
+ * @see ThreadNetworkController#unregisterConfigurationCallback
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+@SystemApi
+public final class ThreadConfiguration implements Parcelable {
+ private final boolean mNat64Enabled;
+ private final boolean mDhcpv6PdEnabled;
+
+ private ThreadConfiguration(Builder builder) {
+ this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
+ }
+
+ private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) {
+ this.mNat64Enabled = nat64Enabled;
+ this.mDhcpv6PdEnabled = dhcpv6PdEnabled;
+ }
+
+ /** Returns {@code true} if NAT64 is enabled. */
+ public boolean isNat64Enabled() {
+ return mNat64Enabled;
+ }
+
+ /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+ public boolean isDhcpv6PdEnabled() {
+ return mDhcpv6PdEnabled;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof ThreadConfiguration)) {
+ return false;
+ } else {
+ ThreadConfiguration otherConfig = (ThreadConfiguration) other;
+ return mNat64Enabled == otherConfig.mNat64Enabled
+ && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append('{');
+ sb.append("Nat64Enabled=").append(mNat64Enabled);
+ sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mNat64Enabled);
+ dest.writeBoolean(mDhcpv6PdEnabled);
+ }
+
+ public static final @NonNull Creator<ThreadConfiguration> CREATOR =
+ new Creator<>() {
+ @Override
+ public ThreadConfiguration createFromParcel(Parcel in) {
+ ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
+ builder.setNat64Enabled(in.readBoolean());
+ builder.setDhcpv6PdEnabled(in.readBoolean());
+ return builder.build();
+ }
+
+ @Override
+ public ThreadConfiguration[] newArray(int size) {
+ return new ThreadConfiguration[size];
+ }
+ };
+
+ /**
+ * The builder for creating {@link ThreadConfiguration} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private boolean mNat64Enabled = false;
+ private boolean mDhcpv6PdEnabled = false;
+
+ /** Creates a new {@link Builder} object with all features disabled. */
+ public Builder() {}
+
+ /**
+ * Creates a new {@link Builder} object from a {@link ThreadConfiguration} object.
+ *
+ * @param config the Border Router configurations to be copied
+ */
+ public Builder(@NonNull ThreadConfiguration config) {
+ Objects.requireNonNull(config);
+
+ mNat64Enabled = config.mNat64Enabled;
+ mDhcpv6PdEnabled = config.mDhcpv6PdEnabled;
+ }
+
+ /**
+ * Enables or disables NAT64 for the device.
+ *
+ * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+ * IPv4.
+ */
+ @NonNull
+ public Builder setNat64Enabled(boolean enabled) {
+ this.mNat64Enabled = enabled;
+ return this;
+ }
+
+ /**
+ * Enables or disables Prefix Delegation for the device.
+ *
+ * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+ * IPv6.
+ */
+ @NonNull
+ public Builder setDhcpv6PdEnabled(boolean enabled) {
+ this.mDhcpv6PdEnabled = enabled;
+ return this;
+ }
+
+ /** Creates a new {@link ThreadConfiguration} object. */
+ @NonNull
+ public ThreadConfiguration build() {
+ return new ThreadConfiguration(this);
+ }
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 8d6b40a..1222398 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -26,6 +26,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.Size;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.os.Binder;
import android.os.OutcomeReceiver;
@@ -34,13 +35,16 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
+import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Provides the primary APIs for controlling all aspects of a Thread network.
@@ -50,7 +54,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ThreadNetworkController {
private static final String TAG = "ThreadNetworkController";
@@ -79,6 +83,25 @@
/** The Thread radio is being disabled. */
public static final int STATE_DISABLING = 2;
+ /** The ephemeral key mode is disabled. */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_DISABLED = 0;
+
+ /**
+ * The ephemeral key mode is enabled, an external commissioner candidate can use the ephemeral
+ * key to connect to this device and get Thread credential shared.
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_ENABLED = 1;
+
+ /**
+ * The ephemeral key is in use. This state means there is already an active secure session
+ * connected to this device with the ephemeral key, it's not possible to use the ephemeral key
+ * for new connections in this state.
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_IN_USE = 2;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -97,14 +120,25 @@
value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
public @interface EnabledState {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = {"EPHEMERAL_KEY_"},
+ value = {EPHEMERAL_KEY_DISABLED, EPHEMERAL_KEY_ENABLED, EPHEMERAL_KEY_IN_USE})
+ public @interface EphemeralKeyState {}
+
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
- /** Minimum value of max power in unit of 0.01dBm. @hide */
- private static final int POWER_LIMITATION_MIN = -32768;
+ /** The value of max power to disable the Thread channel. */
+ // This constant can never change. It has "max" in the name not because it indicates
+ // maximum power, but because it's passed to an API that sets the maximum power to
+ // disabled the Thread channel.
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE;
- /** Maximum value of max power in unit of 0.01dBm. @hide */
- private static final int POWER_LIMITATION_MAX = 32767;
+ /** The maximum lifetime of an ephemeral key. @hide */
+ @NonNull private static final Duration EPHEMERAL_KEY_LIFETIME_MAX = Duration.ofMinutes(10);
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -124,6 +158,12 @@
private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
mOpDatasetCallbackMap = new HashMap<>();
+ private final Object mConfigurationCallbackMapLock = new Object();
+
+ @GuardedBy("mConfigurationCallbackMapLock")
+ private final Map<Consumer<ThreadConfiguration>, ConfigurationCallbackProxy>
+ mConfigurationCallbackMap = new HashMap<>();
+
/** @hide */
public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
requireNonNull(controllerService, "controllerService cannot be null");
@@ -164,6 +204,87 @@
}
}
+ /** Returns the maximum lifetime allowed when activating ephemeral key mode. */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @NonNull
+ public Duration getMaxEphemeralKeyLifetime() {
+ return EPHEMERAL_KEY_LIFETIME_MAX;
+ }
+
+ /**
+ * Activates ephemeral key mode with a given {@code lifetime}. The ephemeral key is a temporary,
+ * single-use numeric code that is used for Thread Administration Sharing. After activation, the
+ * mode may expire or get deactivated, caller to this method should subscribe to the ephemeral
+ * key state updates with {@link #registerStateCallback} to get notified when the ephemeral key
+ * state changes.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The ephemeral
+ * key string contains a sequence of numeric digits 0-9 of user-input friendly length (typically
+ * 9). Subscribers to ephemeral key state updates with {@link #registerStateCallback} will be
+ * notified with a call to {@link #onEphemeralKeyStateChanged}.
+ *
+ * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+ * specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
+ * attached to Thread network
+ * <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
+ * on the device, caller can recover from this error when the ephemeral key mode gets
+ * deactivated
+ * </ul>
+ *
+ * @param lifetime valid lifetime of the generated ephemeral key, should be larger than {@link
+ * Duration#ZERO} and at most the duration returned by {@link #getMaxEphemeralKeyLifetime}.
+ * @param executor the executor on which to execute {@code receiver}
+ * @param receiver the receiver to receive the result of this operation
+ * @throws IllegalArgumentException if the {@code lifetime} exceeds the allowed range
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void activateEphemeralKeyMode(
+ @NonNull Duration lifetime,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ if (lifetime.compareTo(Duration.ZERO) <= 0
+ || lifetime.compareTo(EPHEMERAL_KEY_LIFETIME_MAX) > 0) {
+ throw new IllegalArgumentException(
+ "Invalid ephemeral key lifetime: the value must be in range of (0, "
+ + EPHEMERAL_KEY_LIFETIME_MAX
+ + "]");
+ }
+ long lifetimeMillis = lifetime.toMillis();
+ try {
+ mControllerService.activateEphemeralKeyMode(
+ lifetimeMillis, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deactivates ephemeral key mode. If there is an active connection with the ephemeral key, the
+ * connection will be terminated.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
+ * always succeed if the device is not in ephemeral key mode.
+ *
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive the result of this operation
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void deactivateEphemeralKeyMode(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ try {
+ mControllerService.deactivateEphemeralKeyMode(
+ new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Returns the Thread version this device is operating on. */
@ThreadVersion
public int getThreadVersion() {
@@ -238,6 +359,24 @@
* @param enabledState the new Thread enabled state
*/
default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
+
+ /**
+ * The ephemeral key state has changed.
+ *
+ * @param ephemeralKeyState the ephemeral key state
+ * @param ephemeralKey the ephemeral key string which contains a sequence of numeric digits
+ * 0-9 of user-input friendly length (typically 9), or {@code null} if {@code
+ * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the
+ * permission {@link android.permission.THREAD_NETWORK_PRIVILEGED}
+ * @param expiry a timestamp of when the ephemeral key will expireor {@code null} if {@code
+ * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED}
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ default void onEphemeralKeyStateChanged(
+ @EphemeralKeyState int ephemeralKeyState,
+ @Nullable String ephemeralKey,
+ @Nullable Instant expiry) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
@@ -278,13 +417,37 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ @EphemeralKeyState int ephemeralKeyState, String ephemeralKey, long expiryMillis) {
+ if (!Flags.epskcEnabled()) {
+ throw new IllegalStateException(
+ "This should not be called when Ephemeral key API is disabled");
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ final Instant expiry =
+ ephemeralKeyState == EPHEMERAL_KEY_DISABLED
+ ? null
+ : Instant.ofEpochMilli(expiryMillis);
+
+ try {
+ mExecutor.execute(
+ () ->
+ mCallback.onEphemeralKeyStateChanged(
+ ephemeralKeyState, ephemeralKey, expiry));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
/**
* Registers a callback to be called when Thread network states are changed.
*
- * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
- * existing states.
+ * <p>Upon return of this method, all methods of {@code callback} will be invoked immediately
+ * with existing states. The order of the invoked callbacks is not guaranteed.
*
* @param executor the executor to execute the {@code callback}
* @param callback the callback to receive Thread network state changes
@@ -579,6 +742,95 @@
}
/**
+ * Configures the Thread features for this device.
+ *
+ * <p>This method sets the {@link ThreadConfiguration} for this device. On success, the {@link
+ * OutcomeReceiver#onResult} will be called, and the {@code configuration} will be applied and
+ * persisted to the device; the configuration changes can be observed by {@link
+ * #registerConfigurationCallback}. On failure, {@link OutcomeReceiver#onError} of {@code
+ * receiver} will be invoked with a specific error.
+ *
+ * @param configuration the configuration to set
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ * @hide
+ */
+ // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+ // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ public void setConfiguration(
+ @NonNull ThreadConfiguration configuration,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(configuration, "Configuration cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.setConfiguration(
+ configuration, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Registers a callback to be called when the configuration is changed.
+ *
+ * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
+ * {@link ThreadConfiguration}.
+ *
+ * @param executor the executor to execute the {@code callback}
+ * @param callback the callback to receive Thread configuration changes
+ * @throws IllegalArgumentException if {@code callback} has already been registered
+ */
+ @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+ @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ public void registerConfigurationCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<ThreadConfiguration> callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mConfigurationCallbackMapLock) {
+ if (mConfigurationCallbackMap.containsKey(callback)) {
+ throw new IllegalArgumentException("callback has already been registered");
+ }
+ ConfigurationCallbackProxy callbackProxy =
+ new ConfigurationCallbackProxy(executor, callback);
+ mConfigurationCallbackMap.put(callback, callbackProxy);
+ try {
+ mControllerService.registerConfigurationCallback(callbackProxy);
+ } catch (RemoteException e) {
+ mConfigurationCallbackMap.remove(callback);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the configuration callback.
+ *
+ * @param callback the callback which has been registered with {@link
+ * #registerConfigurationCallback}
+ * @throws IllegalArgumentException if {@code callback} hasn't been registered
+ */
+ @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+ @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mConfigurationCallbackMapLock) {
+ ConfigurationCallbackProxy callbackProxy = mConfigurationCallbackMap.get(callback);
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("callback hasn't been registered");
+ }
+ try {
+ mControllerService.unregisterConfigurationCallback(callbackProxy);
+ mConfigurationCallbackMap.remove(callbackProxy.mConfigurationConsumer);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
* Sets to use a specified test network as the upstream.
*
* @param testNetworkInterfaceName The name of the test network interface. When it's null,
@@ -607,6 +859,10 @@
/**
* Sets max power of each channel.
*
+ * <p>This method sets the max power for the given channel. The platform sets the actual output
+ * power to be less than or equal to the {@code channelMaxPowers} and as close as possible to
+ * the {@code channelMaxPowers}.
+ *
* <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
* chip firmware.
*
@@ -615,22 +871,27 @@
* OutcomeReceiver#onError} will be called with a specific error:
*
* <ul>
- * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
- * supported by the platform.
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the feature is not supported
+ * by the platform.
* </ul>
*
* @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
* and corresponding max power. Valid channel values should be between {@link
* ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
- * ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
- * power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
- * is set to INT16_MAX, the corresponding channel is not supported.
+ * ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. For
+ * example, 1000 means 0.01W and 2000 means 0.1W. If the power value of {@code
+ * channelMaxPowers} is lower than the minimum output power supported by the platform, the
+ * output power will be set to the minimum output power supported by the platform. If the
+ * power value of {@code channelMaxPowers} is higher than the maximum output power supported
+ * by the platform, the output power will be set to the maximum output power supported by
+ * the platform. If the power value of {@code channelMaxPowers} is set to {@link
+ * #MAX_POWER_CHANNEL_DISABLED}, the corresponding channel is disabled.
* @param executor the executor to execute {@code receiver}.
* @param receiver the receiver to receive the result of this operation.
* @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
* or invalid channel or max power is configured.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED)
@RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
public final void setChannelMaxPowers(
@NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
@@ -659,19 +920,6 @@
+ ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
+ "]");
}
-
- if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
- throw new IllegalArgumentException(
- "Channel power ({channel: "
- + channel
- + ", maxPower: "
- + maxPower
- + "}) exceeds allowed range ["
- + POWER_LIMITATION_MIN
- + ", "
- + POWER_LIMITATION_MAX
- + "]");
- }
}
try {
@@ -764,4 +1012,26 @@
propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
}
}
+
+ private static final class ConfigurationCallbackProxy extends IConfigurationReceiver.Stub {
+ final Executor mExecutor;
+ final Consumer<ThreadConfiguration> mConfigurationConsumer;
+
+ ConfigurationCallbackProxy(
+ @CallbackExecutor Executor executor,
+ Consumer<ThreadConfiguration> ConfigurationConsumer) {
+ this.mExecutor = executor;
+ this.mConfigurationConsumer = ConfigurationConsumer;
+ }
+
+ @Override
+ public void onConfigurationChanged(ThreadConfiguration configuration) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mConfigurationConsumer.accept(configuration));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index f699c30..1ea2459 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -23,6 +23,8 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import com.android.net.thread.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -31,7 +33,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public class ThreadNetworkException extends Exception {
/** @hide */
@@ -139,16 +141,14 @@
public static final int ERROR_THREAD_DISABLED = 12;
/**
- * The operation failed because it is not supported by the platform. For example, some platforms
- * may not support setting the target power of each channel. The caller should not retry and may
- * return an error to the user.
- *
- * @hide
+ * The operation failed because the feature is not supported by the platform. For example, some
+ * platforms may not support setting the target power of each channel. The caller should not
+ * retry and may return an error to the user.
*/
- public static final int ERROR_UNSUPPORTED_OPERATION = 13;
+ public static final int ERROR_UNSUPPORTED_FEATURE = 13;
private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
- private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
+ private static final int ERROR_MAX = ERROR_UNSUPPORTED_FEATURE;
private final int mErrorCode;
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
deleted file mode 100644
index e6ab988..0000000
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ /dev/null
@@ -1,31 +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 android.net.thread;
-
-/**
- * Container for flag constants defined in the "thread_network" namespace.
- *
- * @hide
- */
-// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
-// flagging infra is fully supported for mainline modules.
-public final class ThreadNetworkFlags {
- /** @hide */
- public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
-
- private ThreadNetworkFlags() {}
-}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 150b759..b863bc2 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -26,6 +26,7 @@
import android.os.RemoteException;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.thread.flags.Flags;
import java.util.Collections;
import java.util.List;
@@ -35,7 +36,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
@SystemService(ThreadNetworkManager.SERVICE_NAME)
public final class ThreadNetworkManager {
@@ -80,6 +81,19 @@
"android.permission.THREAD_NETWORK_PRIVILEGED";
/**
+ * Permission allows accessing Thread network state and performing certain testing-related
+ * operations.
+ *
+ * <p>This is the same value as android.Manifest.permission.THREAD_NETWORK_TESTING. That symbol
+ * is not available on U while this feature needs to support Android U TV devices, so here is
+ * making a copy of android.Manifest.permission.THREAD_NETWORK_TESTING.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_THREAD_NETWORK_TESTING =
+ "android.permission.THREAD_NETWORK_TESTING";
+
+ /**
* This user restriction specifies if Thread network is disallowed on the device. If Thread
* network is disallowed it cannot be turned on via Settings.
*
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index c176bfa..e012d41 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -1,9 +1,35 @@
#!/usr/bin/env bash
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ANDROID_ROOT_DIR=$(
+ while [ ! -d ".repo" ] && [ "$PWD" != "/" ]; do cd ..; done
+ 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
+if [ ! -d "$ANDROID_ROOT_DIR/.repo" ]; then
+ echo "Error: The script has to run in an Android repo checkout"
+ exit 1
+fi
-$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
-$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
+GOOGLE_JAVA_FORMAT=$ANDROID_ROOT_DIR/prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$ANDROID_ROOT_DIR/prebuilts/build-tools/linux-x86/bin/bpfmt
+AIDL_FORMAT=$ANDROID_ROOT_DIR/system/tools/aidl/aidl-format.sh
+
+CONNECTIVITY_DIR=$ANDROID_ROOT_DIR/packages/modules/Connectivity
+OPENTHREAD_DIR=$ANDROID_ROOT_DIR/external/openthread
+OTBR_POSIX_DIR=$ANDROID_ROOT_DIR/external/ot-br-posix
+
+ALLOWED_CODE_DIRS=($CONNECTIVITY_DIR $OPENTHREAD_DIR $OTBR_POSIX_DIR)
+CODE_DIR=$(git rev-parse --show-toplevel)
+
+if [[ ! " ${ALLOWED_CODE_DIRS[@]} " =~ " ${CODE_DIR} " ]]; then
+ echo "Error: The script has to run in the Git project Connectivity, openthread or ot-br-posix"
+ exit 1
+fi
+
+if [[ $CODE_DIR == $CONNECTIVITY_DIR ]]; then
+ CODE_DIR=$CODE_DIR"/thread"
+fi
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $CODE_DIR -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $CODE_DIR -name "*.bp")
+$AIDL_FORMAT -w $(find $CODE_DIR -name "*.aidl")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index a82a499..1f4e601 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -37,7 +37,7 @@
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"framework-location.stubs.module_lib",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
"ServiceConnectivityResources",
],
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index be54cbc..e72c9ee 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -16,14 +16,30 @@
package com.android.server.thread;
-import android.os.ParcelFileDescriptor;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_RAW;
+import static android.system.OsConstants.IPV6_CHECKSUM;
+import static android.system.OsConstants.IPV6_MULTICAST_HOPS;
+import static android.system.OsConstants.IPV6_RECVHOPLIMIT;
+import static android.system.OsConstants.IPV6_RECVPKTINFO;
+import static android.system.OsConstants.IPV6_UNICAST_HOPS;
+import android.net.util.SocketUtils;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.FileDescriptor;
import java.io.IOException;
/** Controller for the infrastructure network interface. */
public class InfraInterfaceController {
private static final String TAG = "InfraIfController";
+ private static final int ENABLE = 1;
+ private static final int IPV6_CHECKSUM_OFFSET = 2;
+ private static final int HOP_LIMIT = 255;
+
static {
System.loadLibrary("service-thread-jni");
}
@@ -37,8 +53,21 @@
* @throws IOException when fails to create the socket.
*/
public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
- return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+ ParcelFileDescriptor parcelFd =
+ ParcelFileDescriptor.adoptFd(nativeCreateFilteredIcmp6Socket());
+ FileDescriptor fd = parcelFd.getFileDescriptor();
+ try {
+ Os.setsockoptInt(fd, IPPROTO_RAW, IPV6_CHECKSUM, IPV6_CHECKSUM_OFFSET);
+ Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, ENABLE);
+ Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, ENABLE);
+ Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, HOP_LIMIT);
+ Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, HOP_LIMIT);
+ SocketUtils.bindSocketToInterface(fd, infraInterfaceName);
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to setsockopt for the ICMPv6 socket", e);
+ }
+ return parcelFd;
}
- private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+ private static native int nativeCreateFilteredIcmp6Socket() throws IOException;
}
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 1447ff8..9697c02 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.net.DnsResolver;
import android.net.InetAddresses;
+import android.net.LinkProperties;
import android.net.Network;
import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdManager;
@@ -30,11 +31,12 @@
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.RemoteException;
+import android.system.Os;
import android.text.TextUtils;
-import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
import com.android.server.thread.openthread.INsdPublisher;
@@ -62,9 +64,11 @@
*/
public final class NsdPublisher extends INsdPublisher.Stub {
private static final String TAG = NsdPublisher.class.getSimpleName();
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
// TODO: b/321883491 - specify network for mDNS operations
@Nullable private Network mNetwork;
+ private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final NsdManager mNsdManager;
private final DnsResolver mDnsResolver;
private final Handler mHandler;
@@ -75,17 +79,28 @@
private final SparseArray<HostInfoListener> mHostInfoListeners = new SparseArray<>(0);
@VisibleForTesting
- public NsdPublisher(NsdManager nsdManager, DnsResolver dnsResolver, Handler handler) {
+ public NsdPublisher(
+ NsdManager nsdManager,
+ DnsResolver dnsResolver,
+ Handler handler,
+ Map<Network, LinkProperties> networkToLinkProperties) {
mNetwork = null;
mNsdManager = nsdManager;
mDnsResolver = dnsResolver;
mHandler = handler;
mExecutor = runnable -> mHandler.post(runnable);
+ mNetworkToLinkProperties = networkToLinkProperties;
}
- public static NsdPublisher newInstance(Context context, Handler handler) {
+ public static NsdPublisher newInstance(
+ Context context,
+ Handler handler,
+ Map<Network, LinkProperties> networkToLinkProperties) {
return new NsdPublisher(
- context.getSystemService(NsdManager.class), DnsResolver.getInstance(), handler);
+ context.getSystemService(NsdManager.class),
+ DnsResolver.getInstance(),
+ handler,
+ networkToLinkProperties);
}
// TODO: b/321883491 - NsdPublisher should be disabled when mNetwork is null
@@ -158,8 +173,7 @@
int listenerId,
String registrationType) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Registering "
+ registrationType
+ ". Listener ID: "
@@ -171,7 +185,7 @@
try {
mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
} catch (IllegalArgumentException e) {
- Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
+ LOG.e("Failed to register service. serviceInfo: " + serviceInfo, e);
listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
}
}
@@ -184,8 +198,7 @@
checkOnHandlerThread();
RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
if (registrationListener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to unregister service."
+ " Listener ID: "
+ listenerId
@@ -193,8 +206,7 @@
return;
}
- Log.i(
- TAG,
+ LOG.i(
"Unregistering service."
+ " Listener ID: "
+ listenerId
@@ -212,13 +224,7 @@
private void discoverServiceInternal(
String type, INsdDiscoverServiceCallback callback, int listenerId) {
checkOnHandlerThread();
- Log.i(
- TAG,
- "Discovering services."
- + " Listener ID: "
- + listenerId
- + ", service type: "
- + type);
+ LOG.i("Discovering services." + " Listener ID: " + listenerId + ", service type: " + type);
DiscoveryListener listener = new DiscoveryListener(listenerId, type, callback);
mDiscoveryListeners.append(listenerId, listener);
@@ -237,15 +243,14 @@
DiscoveryListener listener = mDiscoveryListeners.get(listenerId);
if (listener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop service discovery. Listener ID "
+ listenerId
+ ". The listener is null.");
return;
}
- Log.i(TAG, "Stopping service discovery. Listener: " + listener);
+ LOG.i("Stopping service discovery. Listener: " + listener);
mNsdManager.stopServiceDiscovery(listener);
}
@@ -263,8 +268,7 @@
serviceInfo.setServiceName(name);
serviceInfo.setServiceType(type);
serviceInfo.setNetwork(null);
- Log.i(
- TAG,
+ LOG.i(
"Resolving service."
+ " Listener ID: "
+ listenerId
@@ -288,21 +292,19 @@
ServiceInfoListener listener = mServiceInfoListeners.get(listenerId);
if (listener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop service resolution. Listener ID: "
+ listenerId
+ ". The listener is null.");
return;
}
- Log.i(TAG, "Stopping service resolution. Listener: " + listener);
+ LOG.i("Stopping service resolution. Listener: " + listener);
try {
mNsdManager.unregisterServiceInfoCallback(listener);
} catch (IllegalArgumentException e) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop the service resolution because it's already stopped. Listener: "
+ listener);
}
@@ -330,7 +332,7 @@
listener);
mHostInfoListeners.append(listenerId, listener);
- Log.i(TAG, "Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
+ LOG.i("Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
}
@Override
@@ -343,14 +345,13 @@
HostInfoListener listener = mHostInfoListeners.get(listenerId);
if (listener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop host resolution. Listener ID: "
+ listenerId
+ ". The listener is null.");
return;
}
- Log.i(TAG, "Stopping host resolution. Listener: " + listener);
+ LOG.i("Stopping host resolution. Listener: " + listener);
listener.cancel();
mHostInfoListeners.remove(listenerId);
}
@@ -373,14 +374,14 @@
try {
mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
} catch (IllegalArgumentException e) {
- Log.i(
- TAG,
+ LOG.i(
"Failed to unregister."
+ " Listener ID: "
+ mRegistrationListeners.keyAt(i)
+ " serviceInfo: "
- + mRegistrationListeners.valueAt(i).mServiceInfo,
- e);
+ + mRegistrationListeners.valueAt(i).mServiceInfo
+ + ", error: "
+ + e.getMessage());
}
}
mRegistrationListeners.clear();
@@ -415,8 +416,7 @@
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
checkOnHandlerThread();
mRegistrationListeners.remove(mListenerId);
- Log.i(
- TAG,
+ LOG.i(
"Failed to register listener ID: "
+ mListenerId
+ " error code: "
@@ -434,8 +434,7 @@
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
checkOnHandlerThread();
for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
- Log.i(
- TAG,
+ LOG.i(
"Failed to unregister."
+ "Listener ID: "
+ mListenerId
@@ -454,8 +453,7 @@
@Override
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Registered successfully. "
+ "Listener ID: "
+ mListenerId
@@ -472,8 +470,7 @@
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
checkOnHandlerThread();
for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
- Log.i(
- TAG,
+ LOG.i(
"Unregistered successfully. "
+ "Listener ID: "
+ mListenerId
@@ -505,8 +502,7 @@
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to start service discovery."
+ " Error code: "
+ errorCode
@@ -517,8 +513,7 @@
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to stop service discovery."
+ " Error code: "
+ errorCode
@@ -529,18 +524,18 @@
@Override
public void onDiscoveryStarted(String serviceType) {
- Log.i(TAG, "Started service discovery. Listener: " + this);
+ LOG.i("Started service discovery. Listener: " + this);
}
@Override
public void onDiscoveryStopped(String serviceType) {
- Log.i(TAG, "Stopped service discovery. Listener: " + this);
+ LOG.i("Stopped service discovery. Listener: " + this);
mDiscoveryListeners.remove(mListenerId);
}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
- Log.i(TAG, "Found service: " + serviceInfo);
+ LOG.i("Found service: " + serviceInfo);
try {
mDiscoverServiceCallback.onServiceDiscovered(
serviceInfo.getServiceName(), mType, true);
@@ -551,7 +546,7 @@
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
- Log.i(TAG, "Lost service: " + serviceInfo);
+ LOG.i("Lost service: " + serviceInfo);
try {
mDiscoverServiceCallback.onServiceDiscovered(
serviceInfo.getServiceName(), mType, false);
@@ -584,8 +579,7 @@
@Override
public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to register service info callback."
+ " Listener ID: "
+ mListenerId
@@ -599,14 +593,21 @@
@Override
public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
- Log.i(
- TAG,
+ LOG.i(
"Service is resolved. "
+ " Listener ID: "
+ mListenerId
+ ", serviceInfo: "
+ serviceInfo);
List<String> addresses = new ArrayList<>();
+ int interfaceIndex = 0;
+ if (mNetworkToLinkProperties.containsKey(serviceInfo.getNetwork())) {
+ interfaceIndex =
+ Os.if_nametoindex(
+ mNetworkToLinkProperties
+ .get(serviceInfo.getNetwork())
+ .getInterfaceName());
+ }
for (InetAddress address : serviceInfo.getHostAddresses()) {
if (address instanceof Inet6Address) {
addresses.add(address.getHostAddress());
@@ -623,6 +624,7 @@
try {
mResolveServiceCallback.onServiceResolved(
serviceInfo.getHostname(),
+ interfaceIndex,
serviceInfo.getServiceName(),
serviceInfo.getServiceType(),
serviceInfo.getPort(),
@@ -640,7 +642,7 @@
@Override
public void onServiceInfoCallbackUnregistered() {
- Log.i(TAG, "The service info callback is unregistered. Listener: " + this);
+ LOG.i("The service info callback is unregistered. Listener: " + this);
mServiceInfoListeners.remove(mListenerId);
}
@@ -671,8 +673,7 @@
public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Host is resolved."
+ " Listener ID: "
+ mListenerId
@@ -698,14 +699,14 @@
public void onError(@NonNull DnsResolver.DnsException error) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Failed to resolve host."
+ " Listener ID: "
+ mListenerId
+ ", hostname: "
- + mHostname,
- error);
+ + mHostname
+ + ", error: "
+ + error.getMessage());
try {
mResolveHostCallback.onHostResolved(mHostname, Collections.emptyList());
} catch (RemoteException e) {
diff --git a/thread/service/java/com/android/server/thread/OutputReceiverWrapper.java b/thread/service/java/com/android/server/thread/OutputReceiverWrapper.java
new file mode 100644
index 0000000..aa9a05d
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/OutputReceiverWrapper.java
@@ -0,0 +1,120 @@
+/*
+ * 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 android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.IOutputReceiver;
+import android.net.thread.ThreadNetworkException;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** A {@link IOutputReceiver} wrapper which makes it easier to invoke the callbacks. */
+final class OutputReceiverWrapper {
+ private final IOutputReceiver mReceiver;
+ private final boolean mExpectOtDaemonDied;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<OutputReceiverWrapper> sPendingReceivers = new HashSet<>();
+
+ public OutputReceiverWrapper(IOutputReceiver receiver) {
+ this(receiver, false /* expectOtDaemonDied */);
+ }
+
+ /**
+ * Creates a new {@link OutputReceiverWrapper}.
+ *
+ * <p>If {@code expectOtDaemonDied} is {@code true}, it's expected that ot-daemon becomes dead
+ * before {@code receiver} is completed with {@code onComplete} and {@code onError} and {@code
+ * receiver#onComplete} will be invoked in this case.
+ */
+ public OutputReceiverWrapper(IOutputReceiver receiver, boolean expectOtDaemonDied) {
+ mReceiver = receiver;
+ mExpectOtDaemonDied = expectOtDaemonDied;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (OutputReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ if (receiver.mExpectOtDaemonDied) {
+ receiver.mReceiver.onComplete();
+ } else {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ }
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onOutput(String output) {
+ try {
+ mReceiver.onOutput(output);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onComplete() {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onComplete();
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(Throwable e) {
+ if (e instanceof ThreadNetworkException) {
+ ThreadNetworkException threadException = (ThreadNetworkException) e;
+ onError(threadException.getErrorCode(), threadException.getMessage());
+ } else if (e instanceof RemoteException) {
+ onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ } else {
+ throw new AssertionError(e);
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage, Object... messageArgs) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0c77dee..3d854d7 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -15,6 +15,7 @@
package com.android.server.thread;
import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
@@ -26,6 +27,7 @@
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.EPHEMERAL_KEY_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -40,9 +42,10 @@
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.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
@@ -91,12 +94,15 @@
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IOutputReceiver;
import android.net.thread.IStateCallback;
import android.net.thread.IThreadNetworkController;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
import android.net.thread.ThreadNetworkException;
@@ -106,27 +112,29 @@
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserManager;
-import android.provider.Settings;
-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.SharedLog;
import com.android.server.ServiceManagerWrapper;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.thread.openthread.BackboneRouterState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import com.android.server.thread.openthread.DnsTxtAttribute;
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.IOtOutputReceiver;
import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.InfraLinkState;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.MeshcopTxtAttributes;
import com.android.server.thread.openthread.OnMeshPrefixConfig;
+import com.android.server.thread.openthread.OtDaemonConfiguration;
import com.android.server.thread.openthread.OtDaemonState;
import libcore.util.HexEncoding;
@@ -137,6 +145,7 @@
import java.time.Clock;
import java.time.DateTimeException;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -156,7 +165,8 @@
*/
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
- private static final String TAG = "ThreadNetworkService";
+ private static final String TAG = "ControllerService";
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
// The model name length in utf-8 bytes
private static final int MAX_MODEL_NAME_UTF8_BYTES = 24;
@@ -189,6 +199,8 @@
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
private final ConnectivityResources mResources;
private final Supplier<String> mCountryCodeSupplier;
+ private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
+ new HashMap<>();
// This should not be directly used for calling IOtDaemon APIs because ot-daemon may die and
// {@code mOtDaemon} will be set to {@code null}. Instead, use {@code getOtDaemon()}
@@ -202,14 +214,14 @@
private NetworkRequest mUpstreamNetworkRequest;
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
- private final HashMap<Network, String> mNetworkToInterface;
+ private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
- private boolean mAirplaneModeOn;
private boolean mForceStopOtDaemonEnabled;
- private BorderRouterConfigurationParcel mBorderRouterConfig;
+ private OtDaemonConfiguration mOtDaemonConfig;
+ private InfraLinkState mInfraLinkState;
@VisibleForTesting
ThreadNetworkControllerService(
@@ -224,7 +236,8 @@
NsdPublisher nsdPublisher,
UserManager userManager,
ConnectivityResources resources,
- Supplier<String> countryCodeSupplier) {
+ Supplier<String> countryCodeSupplier,
+ Map<Network, LinkProperties> networkToLinkProperties) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@@ -233,8 +246,11 @@
mTunIfController = tunIfController;
mInfraIfController = infraIfController;
mUpstreamNetworkRequest = newUpstreamNetworkRequest();
- mNetworkToInterface = new HashMap<Network, String>();
- mBorderRouterConfig = new BorderRouterConfigurationParcel();
+ // TODO: networkToLinkProperties should be shared with NsdPublisher, add a test/assert to
+ // verify they are the same.
+ mNetworkToLinkProperties = networkToLinkProperties;
+ mOtDaemonConfig = new OtDaemonConfiguration.Builder().build();
+ mInfraLinkState = new InfraLinkState.Builder().build();
mPersistentSettings = persistentSettings;
mNsdPublisher = nsdPublisher;
mUserManager = userManager;
@@ -251,6 +267,7 @@
Handler handler = new Handler(handlerThread.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
+ Map<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
return new ThreadNetworkControllerService(
context,
@@ -261,17 +278,20 @@
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController(),
persistentSettings,
- NsdPublisher.newInstance(context, handler),
+ NsdPublisher.newInstance(context, handler, networkToLinkProperties),
context.getSystemService(UserManager.class),
new ConnectivityResources(context),
- countryCodeSupplier);
+ countryCodeSupplier,
+ networkToLinkProperties);
}
private NetworkRequest newUpstreamNetworkRequest() {
- NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+ NetworkRequest.Builder builder = new NetworkRequest.Builder();
if (mUpstreamTestNetworkSpecifier != null) {
- return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ // Test networks don't have NET_CAPABILITY_TRUSTED
+ return builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
.setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
.build();
}
@@ -294,12 +314,12 @@
return;
}
- Log.i(TAG, "Starting OT daemon...");
+ LOG.i("Starting OT daemon...");
try {
getOtDaemon();
} catch (RemoteException e) {
- Log.e(TAG, "Failed to initialize ot-daemon", e);
+ LOG.e("Failed to initialize ot-daemon", e);
} catch (ThreadNetworkException e) {
// no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
throw new AssertionError(e);
@@ -340,8 +360,8 @@
final String modelName = resources.getString(R.string.config_thread_model_name);
final String vendorName = resources.getString(R.string.config_thread_vendor_name);
final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
- final boolean managedByGoogle =
- resources.getBoolean(R.bool.config_thread_managed_by_google_home);
+ final String[] vendorSpecificTxts =
+ resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
if (!modelName.isEmpty()) {
if (modelName.getBytes(UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
@@ -371,26 +391,52 @@
meshcopTxts.modelName = modelName;
meshcopTxts.vendorName = vendorName;
meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
- meshcopTxts.nonStandardTxtEntries = List.of(makeManagedByGoogleTxtAttr(managedByGoogle));
+ meshcopTxts.nonStandardTxtEntries = makeVendorSpecificTxtAttrs(vendorSpecificTxts);
return meshcopTxts;
}
/**
- * Creates a DNS-SD TXT entry for indicating whether Thread on this device is managed by Google.
+ * Parses vendor-specific TXT entries from "=" separated strings into list of {@link
+ * DnsTxtAttribute}.
*
- * @return TXT entry "vgh=1" if {@code managedByGoogle} is {@code true}; otherwise, "vgh=0"
+ * @throws IllegalArgumentsException if invalid TXT entries are found in {@code vendorTxts}
*/
- private static DnsTxtAttribute makeManagedByGoogleTxtAttr(boolean managedByGoogle) {
- final byte[] value = (managedByGoogle ? "1" : "0").getBytes(UTF_8);
- return new DnsTxtAttribute("vgh", value);
+ @VisibleForTesting
+ static List<DnsTxtAttribute> makeVendorSpecificTxtAttrs(String[] vendorTxts) {
+ List<DnsTxtAttribute> txts = new ArrayList<>();
+ for (String txt : vendorTxts) {
+ String[] kv = txt.split("=", 2 /* limit */); // Split with only the first '='
+ if (kv.length < 1) {
+ throw new IllegalArgumentException(
+ "Invalid vendor-specific TXT is found in resources: " + txt);
+ }
+
+ if (kv[0].length() < 2) {
+ throw new IllegalArgumentException(
+ "Invalid vendor-specific TXT key \""
+ + kv[0]
+ + "\": it must contain at least 2 characters");
+ }
+
+ if (!kv[0].startsWith("v")) {
+ throw new IllegalArgumentException(
+ "Invalid vendor-specific TXT key \""
+ + kv[0]
+ + "\": it doesn't start with \"v\"");
+ }
+
+ txts.add(new DnsTxtAttribute(kv[0], (kv.length >= 2 ? kv[1] : "").getBytes(UTF_8)));
+ }
+ return txts;
}
private void onOtDaemonDied() {
checkOnHandlerThread();
- Log.w(TAG, "OT daemon is dead, clean up...");
+ LOG.w("OT daemon is dead, clean up...");
OperationReceiverWrapper.onOtDaemonDied();
+ OutputReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
mTunIfController.onOtDaemonDied();
mNsdPublisher.onOtDaemonDied();
@@ -401,8 +447,7 @@
public void initialize() {
mHandler.post(
() -> {
- Log.d(
- TAG,
+ LOG.v(
"Initializing Thread system service: Thread is "
+ (shouldEnableThread() ? "enabled" : "disabled"));
try {
@@ -413,11 +458,9 @@
}
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
- requestThreadNetwork();
+ registerThreadNetworkCallback();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
- mAirplaneModeOn = isAirplaneModeOn();
- registerAirplaneModeReceiver();
maybeInitializeOtDaemon();
});
}
@@ -461,7 +504,7 @@
// become dead, so that it's guaranteed that ot-daemon is stopped when {@code
// receiver} is completed
} catch (RemoteException e) {
- Log.e(TAG, "otDaemon.terminate failed", e);
+ LOG.e("otDaemon.terminate failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
} catch (ThreadNetworkException e) {
// No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown
@@ -492,32 +535,95 @@
return;
}
- Log.i(TAG, "Set Thread enabled: " + isEnabled + ", persist: " + persist);
+ LOG.i("Set Thread enabled: " + isEnabled + ", persist: " + persist);
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);
-
- // Remember whether the user wanted to keep Thread enabled in airplane mode. If once
- // the user disabled Thread again in airplane mode, the persistent settings state is
- // reset (so that Thread will be auto-disabled again when airplane mode is turned on).
- // This behavior is consistent with Wi-Fi and bluetooth.
- if (mAirplaneModeOn) {
- mPersistentSettings.put(
- ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE.key, isEnabled);
- }
}
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+ LOG.e("otDaemon.setThreadEnabled failed", e);
receiver.onError(e);
}
}
+ @Override
+ public void setConfiguration(
+ @NonNull ThreadConfiguration configuration, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> setConfigurationInternal(configuration, receiver));
+ }
+
+ private void setConfigurationInternal(
+ @NonNull ThreadConfiguration configuration,
+ @NonNull IOperationReceiver operationReceiver) {
+ checkOnHandlerThread();
+
+ LOG.i("Set Thread configuration: " + configuration);
+
+ final boolean changed = mPersistentSettings.putConfiguration(configuration);
+ try {
+ operationReceiver.onSuccess();
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ if (changed) {
+ for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
+ try {
+ configReceiver.onConfigurationChanged(configuration);
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+ // TODO: set the configuration at ot-daemon
+ }
+
+ @Override
+ public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+ enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> registerConfigurationCallbackInternal(callback));
+ }
+
+ private void registerConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+ checkOnHandlerThread();
+ if (mConfigurationReceivers.containsKey(callback)) {
+ throw new IllegalStateException("Registering the same IConfigurationReceiver twice");
+ }
+ IBinder.DeathRecipient deathRecipient =
+ () -> mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ return;
+ }
+ mConfigurationReceivers.put(callback, deathRecipient);
+ try {
+ callback.onConfigurationChanged(mPersistentSettings.getConfiguration());
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void unregisterConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+ enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+ }
+
+ private void unregisterConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+ checkOnHandlerThread();
+ if (!mConfigurationReceivers.containsKey(callback)) {
+ return;
+ }
+ callback.asBinder().unlinkToDeath(mConfigurationReceivers.remove(callback), 0);
+ }
+
private void registerUserRestrictionsReceiver() {
mContext.registerReceiver(
new BroadcastReceiver() {
@@ -536,8 +642,7 @@
if (mUserRestricted == newUserRestrictedState) {
return;
}
- Log.i(
- TAG,
+ LOG.i(
"Thread user restriction changed: "
+ mUserRestricted
+ " -> "
@@ -549,16 +654,14 @@
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
- Log.d(
- TAG,
+ LOG.v(
(shouldEnableThread ? "Enabled" : "Disabled")
+ " Thread due to user restriction change");
}
@Override
public void onError(int errorCode, String errorMessage) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to "
+ (shouldEnableThread ? "enable" : "disable")
+ " Thread for user restriction change");
@@ -575,74 +678,13 @@
return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
}
- private void registerAirplaneModeReceiver() {
- mContext.registerReceiver(
- new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- onAirplaneModeChanged(isAirplaneModeOn());
- }
- },
- new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
- null /* broadcastPermission */,
- mHandler);
- }
-
- private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
- checkOnHandlerThread();
- if (mAirplaneModeOn == newAirplaneModeOn) {
- return;
- }
- Log.i(TAG, "Airplane mode changed: " + mAirplaneModeOn + " -> " + newAirplaneModeOn);
- mAirplaneModeOn = newAirplaneModeOn;
-
- final boolean shouldEnableThread = shouldEnableThread();
- final IOperationReceiver receiver =
- new IOperationReceiver.Stub() {
- @Override
- public void onSuccess() {
- Log.d(
- TAG,
- (shouldEnableThread ? "Enabled" : "Disabled")
- + " Thread due to airplane mode change");
- }
-
- @Override
- public void onError(int errorCode, String errorMessage) {
- Log.e(
- TAG,
- "Failed to "
- + (shouldEnableThread ? "enable" : "disable")
- + " Thread for airplane mode change");
- }
- };
- // Do not save the user restriction state to persistent settings so that the user
- // configuration won't be overwritten
- setEnabledInternal(
- shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
- }
-
- /** Returns {@code true} if Airplane mode has been turned on. */
- private boolean isAirplaneModeOn() {
- return Settings.Global.getInt(
- mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0)
- == 1;
- }
-
/**
* Returns {@code true} if Thread should be enabled based on current settings, runtime user
- * restriction and airplane mode state.
+ * restriction state.
*/
private boolean shouldEnableThread() {
- final boolean enabledInAirplaneMode =
- mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE);
-
return !mForceStopOtDaemonEnabled
&& !mUserRestricted
- // FIXME(b/340744397): Note that here we need to call `isAirplaneModeOn()` to get
- // the latest state of airplane mode but can't use `mIsAirplaneMode`. This is for
- // avoiding the race conditions described in b/340744397
- && (!isAirplaneModeOn() || enabledInAirplaneMode)
&& mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
}
@@ -659,7 +701,7 @@
if (mUpstreamNetworkCallback == null) {
throw new AssertionError("The upstream network request null.");
}
- mNetworkToInterface.clear();
+ mNetworkToLinkProperties.clear();
mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
mUpstreamNetworkCallback = null;
}
@@ -668,33 +710,32 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Upstream network available: " + network);
+ LOG.i("Upstream network available: " + network);
}
@Override
public void onLost(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Upstream network lost: " + network);
+ LOG.i("Upstream network lost: " + network);
// TODO: disable border routing when upsteam network disconnected
}
@Override
public void onLinkPropertiesChanged(
- @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ @NonNull Network network, @NonNull LinkProperties newLinkProperties) {
checkOnHandlerThread();
- String existingIfName = mNetworkToInterface.get(network);
- String newIfName = linkProperties.getInterfaceName();
- if (Objects.equals(existingIfName, newIfName)) {
+ LinkProperties oldLinkProperties = mNetworkToLinkProperties.get(network);
+ if (Objects.equals(oldLinkProperties, newLinkProperties)) {
return;
}
- Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
- mNetworkToInterface.put(network, newIfName);
+ LOG.i("Upstream network changed: " + oldLinkProperties + " -> " + newLinkProperties);
+ mNetworkToLinkProperties.put(network, newLinkProperties);
// TODO: disable border routing if netIfName is null
if (network.equals(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ setInfraLinkState(newInfraLinkStateBuilder(newLinkProperties).build());
}
}
}
@@ -703,42 +744,43 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Thread network is available: " + network);
+ LOG.i("Thread network is available: " + network);
}
@Override
public void onLost(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Thread network is lost: " + network);
- disableBorderRouting();
+ LOG.i("Thread network is lost: " + network);
+ setInfraLinkState(newInfraLinkStateBuilder().build());
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"LocalNetworkInfo of Thread network changed: {threadNetwork: "
+ network
+ ", localNetworkInfo: "
+ localNetworkInfo
+ "}");
if (localNetworkInfo.getUpstreamNetwork() == null) {
- disableBorderRouting();
+ setInfraLinkState(newInfraLinkStateBuilder().build());
return;
}
if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
mUpstreamNetwork = localNetworkInfo.getUpstreamNetwork();
- if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ if (mNetworkToLinkProperties.containsKey(mUpstreamNetwork)) {
+ setInfraLinkState(
+ newInfraLinkStateBuilder(mNetworkToLinkProperties.get(mUpstreamNetwork))
+ .build());
}
mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
}
}
}
- private void requestThreadNetwork() {
+ private void registerThreadNetworkCallback() {
mConnectivityManager.registerNetworkCallback(
new NetworkRequest.Builder()
// clearCapabilities() is needed to remove forbidden capabilities and UID
@@ -776,7 +818,7 @@
return new NetworkAgent(
mContext,
mHandler.getLooper(),
- TAG,
+ LOG.getTag(),
netCaps,
mTunIfController.getLinkProperties(),
newLocalNetworkConfig(),
@@ -793,7 +835,7 @@
mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
- Log.i(TAG, "Registered Thread network");
+ LOG.i("Registered Thread network");
}
private void unregisterThreadNetwork() {
@@ -803,7 +845,7 @@
return;
}
- Log.d(TAG, "Unregistering Thread network agent");
+ LOG.v("Unregistering Thread network agent");
mNetworkAgent.unregister();
mNetworkAgent = null;
@@ -815,6 +857,47 @@
}
@Override
+ public void activateEphemeralKeyMode(long lifetimeMillis, IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ activateEphemeralKeyModeInternal(
+ lifetimeMillis, new OperationReceiverWrapper(receiver)));
+ }
+
+ private void activateEphemeralKeyModeInternal(
+ long lifetimeMillis, OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.activateEphemeralKeyMode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @Override
+ public void deactivateEphemeralKeyMode(IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () -> deactivateEphemeralKeyModeInternal(new OperationReceiverWrapper(receiver)));
+ }
+
+ private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.deactivateEphemeralKeyMode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
ActiveOperationalDatasetReceiverWrapper receiverWrapper =
@@ -829,7 +912,7 @@
try {
getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+ LOG.e("otDaemon.getChannelMasks failed", e);
receiver.onError(e);
}
}
@@ -870,7 +953,7 @@
now = clock.instant();
authoritative = true;
} catch (DateTimeException e) {
- Log.w(TAG, "Failed to get authoritative time", e);
+ LOG.w("Failed to get authoritative time: " + e.getMessage());
}
int panId = random.nextInt(/* bound= */ 0xffff);
@@ -884,9 +967,7 @@
final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
return new ActiveOperationalDataset.Builder()
- .setActiveTimestamp(
- new OperationalDatasetTimestamp(
- now.getEpochSecond() & 0xffffffffffffL, 0, authoritative))
+ .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(now, authoritative))
.setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
.setPanId(panId)
.setNetworkName(networkName)
@@ -965,7 +1046,14 @@
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
- mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+ boolean hasThreadPrivilegedPermission =
+ (mContext.checkCallingOrSelfPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ == PERMISSION_GRANTED);
+
+ mHandler.post(
+ () ->
+ mOtDaemonCallbackProxy.registerStateCallback(
+ stateCallback, hasThreadPrivilegedPermission));
}
@Override
@@ -1014,6 +1102,25 @@
};
}
+ private IOtOutputReceiver newOtOutputReceiver(OutputReceiverWrapper receiver) {
+ return new IOtOutputReceiver.Stub() {
+ @Override
+ public void onOutput(String output) {
+ receiver.onOutput(output);
+ }
+
+ @Override
+ public void onComplete() {
+ receiver.onComplete();
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ receiver.onError(otErrorToAndroidError(otError), message);
+ }
+ };
+ }
+
@ErrorCode
private static int otErrorToAndroidError(int otError) {
// See external/openthread/include/openthread/error.h for OT error definition
@@ -1023,7 +1130,7 @@
case OT_ERROR_BUSY:
return ERROR_BUSY;
case OT_ERROR_NOT_IMPLEMENTED:
- return ERROR_UNSUPPORTED_OPERATION;
+ return ERROR_UNSUPPORTED_FEATURE;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -1063,7 +1170,7 @@
// The otDaemon.join() will leave first if this device is currently attached
getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.join failed", e);
+ LOG.e("otDaemon.join failed", e);
receiver.onError(e);
}
}
@@ -1088,7 +1195,7 @@
.scheduleMigration(
pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+ LOG.e("otDaemon.scheduleMigration failed", e);
receiver.onError(e);
}
}
@@ -1106,7 +1213,7 @@
try {
getOtDaemon().leave(newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.leave failed", e);
+ LOG.e("otDaemon.leave failed", e);
receiver.onError(e);
}
}
@@ -1139,7 +1246,7 @@
try {
getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.setCountryCode failed", e);
+ LOG.e("otDaemon.setCountryCode failed", e);
receiver.onError(e);
}
}
@@ -1149,7 +1256,7 @@
@Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED, NETWORK_SETTINGS);
- Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+ LOG.i("setTestNetworkAsUpstream: " + testNetworkInterfaceName);
mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
}
@@ -1195,63 +1302,76 @@
try {
getOtDaemon().setChannelMaxPowers(channelMaxPowers, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.setChannelMaxPowers failed", e);
+ LOG.e("otDaemon.setChannelMaxPowers failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
- private void enableBorderRouting(String infraIfName) {
- if (mBorderRouterConfig.isBorderRoutingEnabled
- && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
+ private void setInfraLinkState(InfraLinkState newInfraLinkState) {
+ if (mInfraLinkState.equals(newInfraLinkState)) {
return;
}
- Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
- try {
- mBorderRouterConfig.infraInterfaceName = infraIfName;
- mBorderRouterConfig.infraInterfaceIcmp6Socket =
- mInfraIfController.createIcmp6Socket(infraIfName);
- mBorderRouterConfig.isBorderRoutingEnabled = true;
+ LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + newInfraLinkState);
+ setInfraLinkInterfaceName(newInfraLinkState.interfaceName);
+ setInfraLinkNat64Prefix(newInfraLinkState.nat64Prefix);
+ mInfraLinkState = newInfraLinkState;
+ }
+
+ private void setInfraLinkInterfaceName(String newInfraLinkInterfaceName) {
+ if (Objects.equals(mInfraLinkState.interfaceName, newInfraLinkInterfaceName)) {
+ return;
+ }
+ ParcelFileDescriptor infraIcmp6Socket = null;
+ if (newInfraLinkInterfaceName != null) {
+ try {
+ infraIcmp6Socket = mInfraIfController.createIcmp6Socket(newInfraLinkInterfaceName);
+ } catch (IOException e) {
+ LOG.e("Failed to create ICMPv6 socket on infra network interface", e);
+ }
+ }
+ try {
getOtDaemon()
- .configureBorderRouter(
- mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
- } catch (RemoteException | IOException | ThreadNetworkException e) {
- Log.w(TAG, "Failed to enable border routing", e);
+ .setInfraLinkInterfaceName(
+ newInfraLinkInterfaceName,
+ infraIcmp6Socket,
+ new LoggingOtStatusReceiver("setInfraLinkInterfaceName"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("Failed to set infra link interface name " + newInfraLinkInterfaceName, e);
}
}
- private void disableBorderRouting() {
- mUpstreamNetwork = null;
- mBorderRouterConfig.infraInterfaceName = null;
- mBorderRouterConfig.infraInterfaceIcmp6Socket = null;
- mBorderRouterConfig.isBorderRoutingEnabled = false;
+ private void setInfraLinkNat64Prefix(@Nullable String newNat64Prefix) {
+ if (Objects.equals(mInfraLinkState.nat64Prefix, newNat64Prefix)) {
+ return;
+ }
try {
getOtDaemon()
- .configureBorderRouter(
- mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+ .setInfraLinkNat64Prefix(
+ newNat64Prefix, new LoggingOtStatusReceiver("setInfraLinkNat64Prefix"));
} catch (RemoteException | ThreadNetworkException e) {
- Log.w(TAG, "Failed to disable border routing", e);
+ LOG.e("Failed to set infra link NAT64 prefix " + newNat64Prefix, e);
}
}
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
- Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
+ LOG.i("Thread TUN interface becomes " + (isUp ? "up" : "down"));
} catch (IOException e) {
- Log.e(TAG, "Failed to handle Thread interface state changes", e);
+ LOG.e("Failed to handle Thread interface state changes", e);
}
}
private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
if (ThreadNetworkController.isAttached(deviceRole)) {
- Log.i(TAG, "Attached to the Thread network");
+ LOG.i("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.i(TAG, "Detached from the Thread network");
+ LOG.i("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
@@ -1283,13 +1403,38 @@
}
}
+ @RequiresPermission(
+ allOf = {PERMISSION_THREAD_NETWORK_PRIVILEGED, PERMISSION_THREAD_NETWORK_TESTING})
+ public void runOtCtlCommand(
+ @NonNull String command, boolean isInteractive, @NonNull IOutputReceiver receiver) {
+ enforceAllPermissionsGranted(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED, PERMISSION_THREAD_NETWORK_TESTING);
+
+ mHandler.post(
+ () ->
+ runOtCtlCommandInternal(
+ command, isInteractive, new OutputReceiverWrapper(receiver)));
+ }
+
+ private void runOtCtlCommandInternal(
+ String command, boolean isInteractive, @NonNull OutputReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().runOtCtlCommand(command, isInteractive, newOtOutputReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.runOtCtlCommand failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
private void sendLocalNetworkConfig() {
if (mNetworkAgent == null) {
return;
}
final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
- Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
+ LOG.v("Sent localNetworkConfig: " + localNetworkConfig);
}
private void handleMulticastForwardingChanged(BackboneRouterState state) {
@@ -1332,6 +1477,29 @@
return builder.build();
}
+ private static OtDaemonConfiguration.Builder newOtDaemonConfigBuilder(
+ OtDaemonConfiguration config) {
+ return new OtDaemonConfiguration.Builder();
+ }
+
+ private static InfraLinkState.Builder newInfraLinkStateBuilder() {
+ return new InfraLinkState.Builder().setInterfaceName("");
+ }
+
+ private static InfraLinkState.Builder newInfraLinkStateBuilder(
+ @Nullable LinkProperties linkProperties) {
+ if (linkProperties == null) {
+ return newInfraLinkStateBuilder();
+ }
+ String nat64Prefix = null;
+ if (linkProperties.getNat64Prefix() != null) {
+ nat64Prefix = linkProperties.getNat64Prefix().toString();
+ }
+ return new InfraLinkState.Builder()
+ .setInterfaceName(linkProperties.getInterfaceName())
+ .setNat64Prefix(nat64Prefix);
+ }
+
private static final class CallbackMetadata {
private static long gId = 0;
@@ -1340,9 +1508,13 @@
final IBinder.DeathRecipient deathRecipient;
- CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+ final boolean hasThreadPrivilegedPermission;
+
+ CallbackMetadata(
+ IBinder.DeathRecipient deathRecipient, boolean hasThreadPrivilegedPermission) {
this.id = allocId();
this.deathRecipient = deathRecipient;
+ this.hasThreadPrivilegedPermission = hasThreadPrivilegedPermission;
}
private static long allocId() {
@@ -1353,17 +1525,21 @@
}
}
- private static final class ConfigureBorderRouterStatusReceiver extends IOtStatusReceiver.Stub {
- public ConfigureBorderRouterStatusReceiver() {}
+ private static class LoggingOtStatusReceiver extends IOtStatusReceiver.Stub {
+ private final String mAction;
+
+ LoggingOtStatusReceiver(String action) {
+ mAction = action;
+ }
@Override
public void onSuccess() {
- Log.i(TAG, "Configured border router successfully");
+ LOG.i("The action " + mAction + " succeeded");
}
@Override
public void onError(int i, String s) {
- Log.w(TAG, String.format("Failed to configure border router: %d %s", i, s));
+ LOG.w("The action " + mAction + " failed: " + i + " " + s);
}
}
@@ -1380,7 +1556,8 @@
private ActiveOperationalDataset mActiveDataset;
private PendingOperationalDataset mPendingDataset;
- public void registerStateCallback(IStateCallback callback) {
+ public void registerStateCallback(
+ IStateCallback callback, boolean hasThreadPrivilegedPermission) {
checkOnHandlerThread();
if (mStateCallbacks.containsKey(callback)) {
throw new IllegalStateException("Registering the same IStateCallback twice");
@@ -1388,7 +1565,8 @@
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterStateCallback(callback));
- CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ CallbackMetadata callbackMetadata =
+ new CallbackMetadata(deathRecipient, hasThreadPrivilegedPermission);
mStateCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1400,7 +1578,7 @@
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+ LOG.e("otDaemon.registerStateCallback failed", e);
}
}
@@ -1421,7 +1599,8 @@
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterDatasetCallback(callback));
- CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ CallbackMetadata callbackMetadata =
+ new CallbackMetadata(deathRecipient, true /* hasThreadPrivilegedPermission */);
mOpDatasetCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1432,7 +1611,7 @@
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+ LOG.e("otDaemon.registerStateCallback failed", e);
}
}
@@ -1500,16 +1679,18 @@
}
@Override
- public void onStateChanged(OtDaemonState newState, long listenerId) {
+ public void onStateChanged(@NonNull OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));
}
private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
checkOnHandlerThread();
+
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
onThreadEnabledChanged(newState.threadEnabled, listenerId);
+ onEphemeralKeyStateChanged(newState, listenerId);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1524,7 +1705,7 @@
mActiveDataset = newActiveDataset;
} catch (IllegalArgumentException e) {
// Is unlikely that OT will generate invalid Operational Dataset
- Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
+ LOG.wtf("Invalid Active Operational Dataset from OpenThread", e);
}
PendingOperationalDataset newPendingDataset;
@@ -1539,7 +1720,7 @@
mPendingDataset = newPendingDataset;
} catch (IllegalArgumentException e) {
// Is unlikely that OT will generate invalid Operational Dataset
- Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
+ LOG.wtf("Invalid Pending Operational Dataset from OpenThread", e);
}
}
@@ -1585,6 +1766,43 @@
}
}
+ private void onEphemeralKeyStateChanged(OtDaemonState newState, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = isEphemeralKeyStateChanged(mState, newState);
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ String passcode =
+ callbackEntry.getValue().hasThreadPrivilegedPermission
+ ? newState.ephemeralKeyPasscode
+ : null;
+ if (newState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) {
+ passcode = null;
+ }
+ try {
+ callbackEntry
+ .getKey()
+ .onEphemeralKeyStateChanged(
+ newState.ephemeralKeyState,
+ passcode,
+ newState.ephemeralKeyExpiryMillis);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private static boolean isEphemeralKeyStateChanged(
+ OtDaemonState oldState, @NonNull OtDaemonState newState) {
+ if (oldState == null) return true;
+ if (oldState.ephemeralKeyState != newState.ephemeralKeyState) return true;
+ if (oldState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) return false;
+ return (!Objects.equals(oldState.ephemeralKeyPasscode, newState.ephemeralKeyPasscode)
+ || oldState.ephemeralKeyExpiryMillis != newState.ephemeralKeyExpiryMillis);
+ }
+
private void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset, long listenerId) {
checkOnHandlerThread();
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index a194114..2cd34e8 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -38,10 +38,10 @@
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.net.module.util.SharedLog;
import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
@@ -63,7 +63,9 @@
*/
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class ThreadNetworkCountryCode {
- private static final String TAG = "ThreadNetworkCountryCode";
+ private static final String TAG = "CountryCode";
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
+
// To be used when there is no country code available.
@VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
@@ -280,11 +282,11 @@
String countryCode = addresses.get(0).getCountryCode();
if (isValidCountryCode(countryCode)) {
- Log.d(TAG, "Set location country code to: " + countryCode);
+ LOG.v("Set location country code to: " + countryCode);
mLocationCountryCodeInfo =
new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
} else {
- Log.d(TAG, "Received invalid location country code");
+ LOG.v("Received invalid location country code");
mLocationCountryCodeInfo = null;
}
@@ -296,8 +298,7 @@
if ((location == null) || (mGeocoder == null)) return;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
- Log.wtf(
- TAG,
+ LOG.wtf(
"Unexpected call to set country code from the Geocoding location, "
+ "Thread code never runs under T or lower.");
return;
@@ -320,13 +321,13 @@
private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
@Override
public void onActiveCountryCodeChanged(String countryCode) {
- Log.d(TAG, "Wifi country code is changed to " + countryCode);
+ LOG.v("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");
+ LOG.w("WiFi country code " + countryCode + " is invalid");
mWifiCountryCodeInfo = null;
}
@@ -336,7 +337,7 @@
@Override
public void onCountryCodeInactive() {
- Log.d(TAG, "Wifi country code is inactived");
+ LOG.v("Wifi country code is inactived");
synchronized ("ThreadNetworkCountryCode.this") {
mWifiCountryCodeInfo = null;
updateCountryCode(false /* forceUpdate */);
@@ -346,8 +347,7 @@
private synchronized void registerTelephonyCountryCodeCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- Log.wtf(
- TAG,
+ LOG.wtf(
"Unexpected call to register the telephony country code changed callback, "
+ "Thread code never runs under T or lower.");
return;
@@ -387,7 +387,7 @@
mSubscriptionManager.getActiveSubscriptionInfoList();
if (subscriptionInfoList == null) {
- Log.d(TAG, "No SIM card is found");
+ LOG.v("No SIM card is found");
return;
}
@@ -399,11 +399,11 @@
try {
countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
} catch (IllegalArgumentException e) {
- Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+ LOG.e("Failed to get country code for slot index:" + slotIndex, e);
continue;
}
- Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+ LOG.v("Telephony slot " + slotIndex + " country code is " + countryCode);
setTelephonyCountryCodeAndLastKnownCountryCode(
slotIndex, countryCode, null /* lastKnownCountryCode */);
}
@@ -411,8 +411,7 @@
private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
int slotIndex, String countryCode, String lastKnownCountryCode) {
- Log.d(
- TAG,
+ LOG.v(
"Set telephony country code to: "
+ countryCode
+ ", last country code to: "
@@ -522,8 +521,7 @@
@Override
public void onError(int otError, String message) {
- Log.e(
- TAG,
+ LOG.e(
"Error "
+ otError
+ ": "
@@ -545,11 +543,11 @@
CountryCodeInfo countryCodeInfo = pickCountryCode();
if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
- Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+ LOG.i("Ignoring already set country code " + countryCodeInfo.getCountryCode());
return;
}
- Log.i(TAG, "Set country code: " + countryCodeInfo);
+ LOG.i("Set country code: " + countryCodeInfo);
mThreadNetworkControllerService.setCountryCode(
countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
newOperationReceiver(countryCodeInfo));
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java b/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java
new file mode 100644
index 0000000..a765304
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkLogger.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 com.android.server.thread;
+
+import com.android.net.module.util.SharedLog;
+
+/**
+ * The Logger for Thread network.
+ *
+ * <p>Each class should log with its own tag using the logger of
+ * ThreadNetworkLogger.forSubComponent(TAG).
+ */
+public final class ThreadNetworkLogger {
+ private static final String TAG = "ThreadNetwork";
+ private static final SharedLog mLog = new SharedLog(TAG);
+
+ public static SharedLog forSubComponent(String subComponent) {
+ return mLog.forSubComponent(subComponent);
+ }
+
+ // Disable instantiation
+ private ThreadNetworkLogger() {}
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
index 54155ee..1eddebf 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -20,9 +20,12 @@
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IOperationReceiver;
+import android.net.thread.IOutputReceiver;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkException;
+import android.os.Binder;
+import android.os.Process;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -52,6 +55,7 @@
private static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
+ private static final Duration OT_CTL_COMMAND_TIMEOUT = Duration.ofSeconds(5);
private static final String PERMISSION_THREAD_NETWORK_TESTING =
"android.permission.THREAD_NETWORK_TESTING";
@@ -62,7 +66,8 @@
@Nullable private PrintWriter mOutputWriter;
@Nullable private PrintWriter mErrorWriter;
- public ThreadNetworkShellCommand(
+ @VisibleForTesting
+ ThreadNetworkShellCommand(
Context context,
ThreadNetworkControllerService controllerService,
ThreadNetworkCountryCode countryCode) {
@@ -77,6 +82,10 @@
mErrorWriter = errorWriter;
}
+ private static boolean isRootProcess() {
+ return Binder.getCallingUid() == Process.ROOT_UID;
+ }
+
private PrintWriter getOutputWriter() {
return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
}
@@ -107,6 +116,8 @@
pw.println(" Gets country code as a two-letter string");
pw.println(" force-country-code enabled <two-letter code> | disabled ");
pw.println(" Sets country code to <two-letter code> or left for normal value");
+ pw.println(" ot-ctl <subcommand>");
+ pw.println(" Runs ot-ctl command");
}
@Override
@@ -133,6 +144,8 @@
return forceCountryCode();
case "get-country-code":
return getCountryCode();
+ case "ot-ctl":
+ return handleOtCtlCommand();
default:
return handleDefaultCommands(cmd);
}
@@ -248,6 +261,50 @@
return 0;
}
+ private static final class OutputReceiver extends IOutputReceiver.Stub {
+ private final CompletableFuture<Void> future;
+ private final PrintWriter outputWriter;
+
+ public OutputReceiver(CompletableFuture<Void> future, PrintWriter outputWriter) {
+ this.future = future;
+ this.outputWriter = outputWriter;
+ }
+
+ @Override
+ public void onOutput(String output) {
+ outputWriter.print(output);
+ outputWriter.flush();
+ }
+
+ @Override
+ public void onComplete() {
+ future.complete(null);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+ }
+ }
+
+ private int handleOtCtlCommand() {
+ ensureTestingPermission();
+
+ if (!isRootProcess()) {
+ getErrorWriter().println("No access to ot-ctl command");
+ return -1;
+ }
+
+ final String subCommand = String.join(" ", peekRemainingArgs());
+
+ CompletableFuture<Void> completeFuture = new CompletableFuture<>();
+ mControllerService.runOtCtlCommand(
+ subCommand,
+ false /* isInteractive */,
+ new OutputReceiver(completeFuture, getOutputWriter()));
+ return waitForFuture(completeFuture, OT_CTL_COMMAND_TIMEOUT, getErrorWriter());
+ }
+
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index f18aac9..fc18ef9 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -18,16 +18,18 @@
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ApexEnvironment;
import android.content.Context;
+import android.net.thread.ThreadConfiguration;
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.net.module.util.SharedLog;
import com.android.server.connectivity.ConnectivityResources;
import java.io.ByteArrayInputStream;
@@ -46,6 +48,7 @@
*/
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
/** File name used for storing settings. */
private static final String FILE_NAME = "ThreadPersistentSettings.xml";
@@ -74,6 +77,16 @@
/** Stores the Thread country code, null if no country code is stored. */
public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
+ /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
+ private static final Key<Boolean> CONFIG_NAT64_ENABLED =
+ new Key<>("config_nat64_enabled", false);
+
+ /**
+ * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
+ */
+ private static final Key<Boolean> CONFIG_DHCP6_PD_ENABLED =
+ new Key<>("config_dhcp6_pd_enabled", false);
+
/******** Thread persistent setting keys ***************/
@GuardedBy("mLock")
@@ -103,7 +116,7 @@
readFromStoreFile();
synchronized (mLock) {
if (!mSettings.containsKey(THREAD_ENABLED.key)) {
- Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+ LOG.i("\"thread_enabled\" is missing in settings file, using default value");
put(
THREAD_ENABLED.key,
mResources.get().getBoolean(R.bool.config_thread_default_enabled));
@@ -175,6 +188,30 @@
}
/**
+ * Store a {@link ThreadConfiguration} to the persistent settings.
+ *
+ * @param configuration {@link ThreadConfiguration} to be stored.
+ * @return {@code true} if the configuration was changed, {@code false} otherwise.
+ */
+ public boolean putConfiguration(@NonNull ThreadConfiguration configuration) {
+ if (getConfiguration().equals(configuration)) {
+ return false;
+ }
+ putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
+ putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
+ writeToStoreFile();
+ return true;
+ }
+
+ /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
+ public ThreadConfiguration getConfiguration() {
+ return new ThreadConfiguration.Builder()
+ .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
+ .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+ .build();
+ }
+
+ /**
* Base class to store string key and its default value.
*
* @param <T> Type of the value.
@@ -207,7 +244,7 @@
writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
}
} catch (IOException e) {
- Log.wtf(TAG, "Write to store file failed", e);
+ LOG.wtf("Write to store file failed", e);
}
}
@@ -215,7 +252,7 @@
try {
final byte[] readData;
synchronized (mLock) {
- Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+ LOG.i("Reading from store file: " + mAtomicFile.getBaseFile());
readData = readFromAtomicFile(mAtomicFile);
}
final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
@@ -226,9 +263,9 @@
mSettings.putAll(bundleRead);
}
} catch (FileNotFoundException e) {
- Log.w(TAG, "No store file to read", e);
+ LOG.w("No store file to read " + e.getMessage());
} catch (IOException e) {
- Log.e(TAG, "Read from store file failed", e);
+ LOG.e("Read from store file failed", e);
}
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 976f93d..85a0371 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -38,10 +38,10 @@
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
-import android.util.Log;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructIfinfoMsg;
import com.android.net.module.util.netlink.StructNlAttr;
@@ -66,6 +66,7 @@
public class TunInterfaceController {
private static final String TAG = "TunIfController";
private static final boolean DBG = false;
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
private static final long INFINITE_LIFETIME = 0xffffffffL;
static final int MTU = 1280;
@@ -147,7 +148,7 @@
/** Adds a new address to the interface. */
public void addAddress(LinkAddress address) {
- Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+ LOG.v("Adding address " + address + " with flags: " + address.getFlags());
long preferredLifetimeSeconds;
long validLifetimeSeconds;
@@ -180,7 +181,7 @@
(byte) address.getScope(),
preferredLifetimeSeconds,
validLifetimeSeconds)) {
- Log.w(TAG, "Failed to add address " + address.getAddress().getHostAddress());
+ LOG.w("Failed to add address " + address.getAddress().getHostAddress());
return;
}
mLinkProperties.addLinkAddress(address);
@@ -189,7 +190,7 @@
/** Removes an address from the interface. */
public void removeAddress(LinkAddress address) {
- Log.d(TAG, "Removing address " + address);
+ LOG.v("Removing address " + address);
// 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
@@ -200,7 +201,7 @@
Os.if_nametoindex(mIfName),
(Inet6Address) address.getAddress(),
(short) address.getPrefixLength())) {
- Log.w(TAG, "Failed to remove address " + address.getAddress().getHostAddress());
+ LOG.w("Failed to remove address " + address.getAddress().getHostAddress());
}
}
@@ -287,7 +288,7 @@
try {
setInterfaceUp(false);
} catch (IOException e) {
- Log.e(TAG, "Failed to set Thread TUN interface down");
+ LOG.e("Failed to set Thread TUN interface down");
}
}
@@ -347,11 +348,15 @@
if (e.getCause() instanceof ErrnoException) {
ErrnoException ee = (ErrnoException) e.getCause();
if (ee.errno == EADDRINUSE) {
- Log.w(TAG, "Already joined group" + address.getHostAddress(), e);
+ LOG.w(
+ "Already joined group "
+ + address.getHostAddress()
+ + ": "
+ + e.getMessage());
return;
}
}
- Log.e(TAG, "failed to join group " + address.getHostAddress(), e);
+ LOG.e("failed to join group " + address.getHostAddress(), e);
}
}
@@ -360,7 +365,7 @@
try {
mMulticastSocket.leaveGroup(socketAddress, mNetworkInterface);
} catch (IOException e) {
- Log.e(TAG, "failed to leave group " + address.getHostAddress(), e);
+ LOG.e("failed to leave group " + address.getHostAddress(), e);
}
}
@@ -415,14 +420,14 @@
}
if (DBG) {
- Log.d(TAG, "ADDR_GEN_MODE message is:");
- Log.d(TAG, HexDump.dumpHexString(msg));
+ LOG.v("ADDR_GEN_MODE message is:");
+ LOG.v(HexDump.dumpHexString(msg));
}
try {
NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
} catch (ErrnoException e) {
- Log.e(TAG, "Failed to set ADDR_GEN_MODE to NONE", e);
+ LOG.e("Failed to set ADDR_GEN_MODE to NONE", e);
}
}
}
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
index 5d24eab..1f260f2 100644
--- a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -42,15 +42,8 @@
namespace android {
static jint
-com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
- jstring interfaceName) {
- ScopedUtfChars ifName(env, interfaceName);
-
- struct icmp6_filter filter;
- constexpr int kEnable = 1;
- constexpr int kIpv6ChecksumOffset = 2;
- constexpr int kHopLimit = 255;
-
+com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket(JNIEnv *env,
+ jobject clazz) {
// Initializes the ICMPv6 socket.
int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
if (sock == -1) {
@@ -59,6 +52,7 @@
return -1;
}
+ struct icmp6_filter filter;
// Only accept Router Advertisements, Router Solicitations and Neighbor
// Advertisements.
ICMP6_FILTER_SETBLOCKALL(&filter);
@@ -73,53 +67,6 @@
return -1;
}
- // We want a source address and interface index.
-
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
- jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
- strerror(errno));
- close(sock);
- return -1;
- }
-
- if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
- sizeof(kIpv6ChecksumOffset)) != 0) {
- jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
- strerror(errno));
- close(sock);
- return -1;
- }
-
- // We need to be able to reject RAs arriving from off-link.
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
- jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
- strerror(errno));
- close(sock);
- return -1;
- }
-
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
- jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
- strerror(errno));
- close(sock);
- return -1;
- }
-
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
- strerror(errno));
- close(sock);
- return -1;
- }
-
- if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
- jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
- strerror(errno));
- close(sock);
- return -1;
- }
-
return sock;
}
@@ -129,8 +76,8 @@
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
- {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
- (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+ {"nativeCreateFilteredIcmp6Socket", "()I",
+ (void *)com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket},
};
int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index c1cf0a0..2630d21 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,9 +21,11 @@
android_test {
name: "CtsThreadNetworkTestCases",
- defaults: ["cts_defaults"],
+ defaults: [
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
min_sdk_version: "33",
- sdk_version: "test_current",
manifest: "AndroidManifest.xml",
test_config: "AndroidTest.xml",
srcs: [
@@ -38,6 +40,7 @@
static_libs: [
"androidx.test.ext.junit",
"compatibility-device-util-axt",
+ "com.android.net.thread.flags-aconfig-java",
"ctstestrunner-axt",
"guava",
"guava-android-testlib",
@@ -46,8 +49,8 @@
"truth",
],
libs: [
- "android.test.base",
- "android.test.runner",
+ "android.test.base.stubs",
+ "android.test.runner.stubs",
"framework-connectivity-module-api-stubs-including-flagged",
],
// Test coverage system runs on different devices. Need to
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 6eda1e9..e954d3b 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -22,6 +22,7 @@
<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" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<!--
Only run tests if the device under test is SDK version 33 (Android 13) or above.
@@ -55,4 +56,14 @@
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
+
+ <!--
+ This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
+ enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
+ `epskc_enabled` flag is rolled out.
+ -->
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+ <option name="flag-value"
+ value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
+ </target_preparer>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
new file mode 100644
index 0000000..386412e
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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 com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.thread.ThreadConfiguration;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadConfiguration}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(Parameterized.class)
+public final class ThreadConfigurationTest {
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ public final boolean mIsNat64Enabled;
+ public final boolean mIsDhcpv6PdEnabled;
+
+ @Parameterized.Parameters
+ public static Collection configArguments() {
+ return Arrays.asList(
+ new Object[][] {
+ {false, false}, // All disabled
+ {true, false}, // NAT64 enabled
+ {false, true}, // DHCP6-PD enabled
+ {true, true}, // All enabled
+ });
+ }
+
+ public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+ mIsNat64Enabled = isNat64Enabled;
+ mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(mIsNat64Enabled)
+ .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+ .build();
+ assertParcelingIsLossless(config);
+ }
+
+ @Test
+ public void builder_correctValuesAreSet() {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(mIsNat64Enabled)
+ .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+ .build();
+
+ assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
+ assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
+ }
+
+ @Test
+ public void builderConstructor_configsAreEqual() {
+ ThreadConfiguration config1 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(mIsNat64Enabled)
+ .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+ .build();
+ ThreadConfiguration config2 = new ThreadConfiguration.Builder(config1).build();
+ assertThat(config1).isEqualTo(config2);
+ }
+}
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 0e95703..1792bfb 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -27,14 +27,18 @@
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.EPHEMERAL_KEY_DISABLED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_ENABLED;
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;
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 android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -45,6 +49,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
@@ -57,6 +62,7 @@
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
@@ -68,14 +74,21 @@
import android.os.Build;
import android.os.HandlerThread;
import android.os.OutcomeReceiver;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.SparseIntArray;
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.net.thread.flags.Flags;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
+import kotlin.Triple;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -90,13 +103,16 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/** CTS tests for {@link ThreadNetworkController}. */
@@ -109,14 +125,30 @@
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 SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
+ private static final int VALID_POWER = 32_767;
+ private static final int VALID_CHANNEL = 20;
+ private static final int INVALID_CHANNEL = 10;
private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
+ private static final ThreadConfiguration DEFAULT_CONFIG =
+ new ThreadConfiguration.Builder().build();
+ private static final SparseIntArray CHANNEL_MAX_POWERS =
+ new SparseIntArray() {
+ {
+ put(VALID_CHANNEL, VALID_POWER);
+ }
+ };
+ private static final Duration EPHEMERAL_KEY_LIFETIME = Duration.ofSeconds(1);
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkController mController;
@@ -126,6 +158,9 @@
private HandlerThread mHandlerThread;
private TapTestNetworkTracker mTestNetworkTracker;
+ private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
+ new ArrayList<>();
+
@Before
public void setUp() throws Exception {
mController =
@@ -140,6 +175,8 @@
mHandlerThread.start();
setEnabledAndWait(mController, true);
+ setConfigurationAndWait(mController, DEFAULT_CONFIG);
+ deactivateEphemeralKeyModeAndWait(mController);
}
@After
@@ -147,6 +184,19 @@
dropAllPermissions();
leaveAndWait(mController);
tearDownTestNetwork();
+ setConfigurationAndWait(mController, DEFAULT_CONFIG);
+ for (Consumer<ThreadConfiguration> configurationCallback :
+ mConfigurationCallbacksToCleanUp) {
+ try {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterConfigurationCallback(configurationCallback));
+ } catch (IllegalArgumentException e) {
+ // Ignore the exception when the callback is not registered.
+ }
+ }
+ mConfigurationCallbacksToCleanUp.clear();
+ deactivateEphemeralKeyModeAndWait(mController);
}
@Test
@@ -783,6 +833,221 @@
listener.unregisterStateCallback();
}
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void getMaxEphemeralKeyLifetime_isLargerThanZero() {
+ assertThat(mController.getMaxEphemeralKeyLifetime()).isGreaterThan(Duration.ZERO);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> startFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME,
+ mExecutor,
+ newOutcomeReceiver(startFuture)));
+
+ startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.activateEphemeralKeyMode(Duration.ZERO, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+ Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.activateEphemeralKeyMode(lifetime, Runnable::run, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
+ throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> future1 = new CompletableFuture<>();
+ CompletableFuture<Void> future2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future1));
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future2));
+ });
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> {
+ future2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ });
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
+ CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+ CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+ CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ stateFuture.complete(state);
+ ephemeralKeyFuture.complete(ephemeralKey);
+ expiryFuture.complete(expiry);
+ }
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
+ assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(EPHEMERAL_KEY_DISABLED);
+ assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(expiryFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
+ throws Exception {
+ CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+ CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+ CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ stateFuture.complete(state);
+ ephemeralKeyFuture.complete(ephemeralKey);
+ expiryFuture.complete(expiry);
+ }
+ };
+ joinRandomizedDatasetAndWait(mController);
+ activateEphemeralKeyModeAndWait(mController);
+
+ runAsShell(
+ ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
+ assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(EPHEMERAL_KEY_ENABLED);
+ assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(
+ expiryFuture
+ .get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)
+ .isAfter(Instant.now()))
+ .isTrue();
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
+ EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
+ joinRandomizedDatasetAndWait(mController);
+
+ try {
+ activateEphemeralKeyModeAndWait(mController);
+ deactivateEphemeralKeyModeAndWait(mController);
+
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+ } finally {
+ listener.unregisterStateCallback();
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
+ EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
+ Triple<Integer, String, Instant> epskc1;
+ try {
+ joinRandomizedDatasetAndWait(mController);
+ activateEphemeralKeyModeAndWait(mController);
+ epskc1 = listener1.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+ } finally {
+ listener1.unregisterStateCallback();
+ }
+
+ EphemeralKeyStateListener listener2 = new EphemeralKeyStateListener(mController);
+ try {
+ Triple<Integer, String, Instant> epskc2 =
+ listener2.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+
+ assertThat(epskc2.getSecond()).isEqualTo(epskc1.getSecond());
+ assertThat(epskc2.getThird()).isEqualTo(epskc1.getThird());
+ } finally {
+ listener2.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
@@ -831,6 +1096,152 @@
NET_CAPABILITY_TRUSTED);
}
+ @Test
+ public void setConfiguration_null_throwsNullPointerException() throws Exception {
+ CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ mController.setConfiguration(
+ null, mExecutor, newOutcomeReceiver(setConfigFuture)));
+ }
+
+ @Test
+ public void setConfiguration_noPermissions_throwsSecurityException() throws Exception {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+ assertThrows(
+ SecurityException.class,
+ () -> {
+ mController.setConfiguration(
+ configuration, mExecutor, newOutcomeReceiver(setConfigFuture));
+ });
+ }
+
+ @Test
+ public void registerConfigurationCallback_permissionsGranted_returnsCurrentStatus()
+ throws Exception {
+ CompletableFuture<ThreadConfiguration> getConfigFuture = new CompletableFuture<>();
+ Consumer<ThreadConfiguration> callback = getConfigFuture::complete;
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> registerConfigurationCallback(mController, mExecutor, callback));
+ assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(DEFAULT_CONFIG);
+ }
+
+ @Test
+ public void registerConfigurationCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> registerConfigurationCallback(mController, mExecutor, config -> {}));
+ }
+
+ @Test
+ public void registerConfigurationCallback_returnsUpdatedConfigurations() throws Exception {
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+ ConfigurationListener listener = new ConfigurationListener(mController);
+ ThreadConfiguration config1 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcpv6PdEnabled(true)
+ .build();
+ ThreadConfiguration config2 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .setDhcpv6PdEnabled(true)
+ .build();
+
+ try {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setConfiguration(
+ config1, mExecutor, newOutcomeReceiver(setFuture1)));
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setConfiguration(
+ config2, mExecutor, newOutcomeReceiver(setFuture2)));
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ listener.expectConfiguration(DEFAULT_CONFIG);
+ listener.expectConfiguration(config1);
+ listener.expectConfiguration(config2);
+ listener.expectNoMoreConfiguration();
+ } finally {
+ listener.unregisterConfigurationCallback();
+ }
+ }
+
+ @Test
+ public void registerConfigurationCallback_alreadyRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ Consumer<ThreadConfiguration> callback = config -> {};
+ registerConfigurationCallback(mController, mExecutor, callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> registerConfigurationCallback(mController, mExecutor, callback));
+ }
+
+ @Test
+ public void unregisterConfigurationCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ Consumer<ThreadConfiguration> callback = config -> {};
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> registerConfigurationCallback(mController, mExecutor, callback));
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.unregisterConfigurationCallback(callback));
+ }
+
+ @Test
+ public void unregisterConfigurationCallback_callbackRegistered_success() throws Exception {
+ Consumer<ThreadConfiguration> callback = config -> {};
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ registerConfigurationCallback(mController, mExecutor, callback);
+ mController.unregisterConfigurationCallback(callback);
+ });
+ }
+
+ @Test
+ public void
+ unregisterConfigurationCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ Consumer<ThreadConfiguration> callback = config -> {};
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterConfigurationCallback(callback));
+ }
+
+ @Test
+ public void unregisterConfigurationCallback_alreadyUnregistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ Consumer<ThreadConfiguration> callback = config -> {};
+ registerConfigurationCallback(mController, mExecutor, callback);
+ mController.unregisterConfigurationCallback(callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterConfigurationCallback(callback));
+ }
+
private void grantPermissions(String... permissions) {
for (String permission : permissions) {
mGrantedPermissions.add(permission);
@@ -858,7 +1269,6 @@
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
- assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
}
@Test
@@ -885,7 +1295,6 @@
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
assertThat(txtMap.get("id").length).isEqualTo(16);
- assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
}
@Test
@@ -1039,6 +1448,35 @@
}
}
+ private class ConfigurationListener {
+ private ArrayTrackRecord<ThreadConfiguration> mConfigurations = new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<ThreadConfiguration>.ReadHead mReadHead =
+ mConfigurations.newReadHead();
+ ThreadNetworkController mController;
+ Consumer<ThreadConfiguration> mCallback = (config) -> mConfigurations.add(config);
+
+ ConfigurationListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerConfigurationCallback(mExecutor, mCallback));
+ }
+
+ public void expectConfiguration(ThreadConfiguration config) {
+ assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> c.equals(config))).isNotNull();
+ }
+
+ public void expectNoMoreConfiguration() {
+ assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> true)).isNull();
+ }
+
+ public void unregisterConfigurationCallback() {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterConfigurationCallback(mCallback));
+ }
+ }
+
private int booleanToEnabledState(boolean enabled) {
return enabled ? STATE_ENABLED : STATE_DISABLED;
}
@@ -1053,6 +1491,83 @@
waitForEnabledState(controller, booleanToEnabledState(enabled));
}
+ private void setConfigurationAndWait(
+ ThreadNetworkController controller, ThreadConfiguration configuration)
+ throws Exception {
+ CompletableFuture<Void> setFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.setConfiguration(
+ configuration, mExecutor, newOutcomeReceiver(setFuture)));
+ setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void deactivateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+ throws Exception {
+ CompletableFuture<Void> clearFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.deactivateEphemeralKeyMode(
+ mExecutor, newOutcomeReceiver(clearFuture)));
+ clearFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void activateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+ throws Exception {
+ CompletableFuture<Void> startFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME,
+ mExecutor,
+ newOutcomeReceiver(startFuture)));
+ startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private class EphemeralKeyStateListener {
+ private ArrayTrackRecord<Triple<Integer, String, Instant>> mEphemeralKeyStates =
+ new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Triple<Integer, String, Instant>>.ReadHead mReadHead =
+ mEphemeralKeyStates.newReadHead();
+ ThreadNetworkController mController;
+ StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ mEphemeralKeyStates.add(new Triple<>(state, ephemeralKey, expiry));
+ }
+ };
+
+ EphemeralKeyStateListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerStateCallback(mExecutor, mCallback));
+ }
+
+ // Expect that EphemeralKey has the expected state, and return a Triple of <state,
+ // passcode, expiry>.
+ public Triple<Integer, String, Instant> expectThreadEphemeralKeyMode(int state) {
+ Triple<Integer, String, Instant> epskc =
+ mReadHead.poll(
+ ENABLED_TIMEOUT_MILLIS, e -> Objects.equals(e.getFirst(), state));
+ assertThat(epskc).isNotNull();
+ return epskc;
+ }
+
+ public void unregisterStateCallback() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+ }
+
private CompletableFuture joinRandomizedDataset(
ThreadNetworkController controller, String networkName) throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
@@ -1119,6 +1634,14 @@
};
}
+ private void registerConfigurationCallback(
+ ThreadNetworkController controller,
+ Executor executor,
+ Consumer<ThreadConfiguration> callback) {
+ controller.registerConfigurationCallback(executor, callback);
+ mConfigurationCallbacksToCleanUp.add(callback);
+ }
+
private static void assertDoesNotThrow(ThrowingRunnable runnable) {
try {
runnable.run();
@@ -1244,4 +1767,52 @@
@Override
public void onServiceInfoCallbackUnregistered() {}
}
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+ public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
+ CompletableFuture<Void> powerFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setChannelMaxPowers(
+ CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
+
+ try {
+ assertThat(powerFuture.get()).isNull();
+ } catch (ExecutionException exception) {
+ ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_FEATURE);
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+ public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+ public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
+ final SparseIntArray INVALID_CHANNEL_ARRAY =
+ new SparseIntArray() {
+ {
+ put(INVALID_CHANNEL, VALID_POWER);
+ }
+ };
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
+ }
}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 71693af..8f082a4 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -37,9 +37,9 @@
"ot-daemon-aidl-java",
],
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
],
}
@@ -58,6 +58,7 @@
],
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
],
compile_multilib: "both",
}
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 8c63d37..4a8462d8 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,23 +17,25 @@
package android.net.thread;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
-import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isFrom;
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
-import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+import static android.net.thread.utils.IntegrationTestUtils.isTo;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
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 android.system.OsConstants.ICMP_ECHO;
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.TestNetworkTrackerKt.initTestNetwork;
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 org.junit.Assert.assertEquals;
@@ -48,9 +50,10 @@
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
-import android.net.MacAddress;
+import android.net.RouteInfo;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.InfraNetworkDevice;
+import android.net.thread.utils.IntegrationTestUtils;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting;
@@ -74,7 +77,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -89,22 +94,19 @@
private static final String TAG = BorderRoutingTest.class.getSimpleName();
private static final int NUM_FTD = 2;
private static final Inet6Address GROUP_ADDR_SCOPE_5 =
- (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+ (Inet6Address) parseNumericAddress("ff05::1234");
private static final Inet6Address GROUP_ADDR_SCOPE_4 =
- (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+ (Inet6Address) 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);
+ (Inet6Address) parseNumericAddress("ff03::1234");
+ private static final Inet4Address IPV4_SERVER_ADDR =
+ (Inet4Address) parseNumericAddress("8.8.8.8");
+ private static final String NAT64_CIDR = "192.168.255.0/24";
+ private static final IpPrefix DHCP6_PD_PREFIX = new IpPrefix("2001:db8::/64");
+ private static final IpPrefix AIL_NAT64_PREFIX = new IpPrefix("2001:db8:1234::/96");
+ private static final Inet6Address AIL_NAT64_SYNTHESIZED_SERVER_ADDR =
+ (Inet6Address) parseNumericAddress("2001:db8:1234::8.8.8.8");
+ private static final Duration UPDATE_NAT64_PREFIX_TIMEOUT = Duration.ofSeconds(10);
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
@@ -171,12 +173,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
// Infra device receives an echo reply sent by FTD.
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -193,11 +195,11 @@
startInfraDeviceAndWaitForOnLinkAddr();
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -213,7 +215,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = ftd.getOmrAddress();
// Create a new infra network and let Thread prefer it
TestNetworkTracker oldInfraNetworkTracker = mInfraNetworkTracker;
@@ -224,7 +226,7 @@
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
} finally {
runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
}
@@ -243,7 +245,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
@@ -285,7 +287,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
@@ -307,7 +309,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
@@ -328,12 +330,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -360,12 +362,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
- assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -382,11 +384,11 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
- assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -407,22 +409,24 @@
*/
FullThreadDevice ftd1 = mFtds.get(0);
- startFtdChild(ftd1);
+ joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
FullThreadDevice ftd2 = mFtds.get(1);
- startFtdChild(ftd2);
+ joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(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()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
}
@Test
@@ -443,21 +447,23 @@
*/
FullThreadDevice ftd1 = mFtds.get(0);
- startFtdChild(ftd1);
+ joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
FullThreadDevice ftd2 = mFtds.get(1);
- startFtdChild(ftd2);
+ joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(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()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
}
@Test
@@ -473,16 +479,18 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
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));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
assertNotNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
}
@Test
@@ -499,12 +507,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.ping(GROUP_ADDR_SCOPE_3);
assertNull(
- pollForPacketOnInfraNetwork(
+ pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
}
@@ -521,14 +529,15 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdLla = ftd.getLinkLocalAddress();
assertNotNull(ftdLla);
ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
assertNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
}
@Test
@@ -544,7 +553,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
assertFalse(ftdMlas.isEmpty());
@@ -552,7 +561,7 @@
ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
assertNull(
- pollForPacketOnInfraNetwork(
+ pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
}
}
@@ -571,7 +580,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
Inet6Address ftdOmr = ftd.getOmrAddress();
@@ -583,7 +592,7 @@
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
}
@Test
@@ -599,7 +608,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = ftd.getOmrAddress();
// Destroy infra link and re-create
@@ -611,38 +620,72 @@
ftd.ping(GROUP_ADDR_SCOPE_4);
assertNotNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded() throws Exception {
+ FullThreadDevice ftd = mFtds.get(0);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+ // TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
+ mOtCtl.setNat64Cidr(NAT64_CIDR);
+ mOtCtl.setNat64Enabled(true);
+ waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
+
+ ftd.ping(IPV4_SERVER_ADDR);
+
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, IPV4_SERVER_ADDR));
+ }
+
+ @Test
+ public void nat64_withAilNat64Prefix_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded()
+ throws Exception {
+ tearDownInfraNetwork();
+ LinkProperties lp = new LinkProperties();
+ // NAT64 feature requires the infra network to have an IPv4 default route.
+ lp.addRoute(
+ new RouteInfo(
+ new IpPrefix("0.0.0.0/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST,
+ 1500 /* mtu */));
+ lp.addRoute(
+ new RouteInfo(
+ new IpPrefix("::/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST,
+ 1500 /* mtu */));
+ lp.setNat64Prefix(AIL_NAT64_PREFIX);
+ mInfraNetworkTracker = IntegrationTestUtils.setUpInfraNetwork(mContext, mController, lp);
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ FullThreadDevice ftd = mFtds.get(0);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+ // TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
+ mOtCtl.setNat64Enabled(true);
+ mOtCtl.addPrefixInNetworkData(DHCP6_PD_PREFIX, "paros", "med");
+ waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
+
+ ftd.ping(IPV4_SERVER_ADDR);
+
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, null, AIL_NAT64_SYNTHESIZED_SERVER_ADDR));
}
private void setUpInfraNetwork() throws Exception {
- mInfraNetworkTracker =
- runAsShell(
- MANAGE_TEST_NETWORKS,
- () ->
- initTestNetwork(
- mContext, new LinkProperties(), 5000 /* timeoutMs */));
- mController.setTestNetworkAsUpstreamAndWait(
- mInfraNetworkTracker.getTestIface().getInterfaceName());
+ mInfraNetworkTracker = IntegrationTestUtils.setUpInfraNetwork(mContext, mController);
}
private void tearDownInfraNetwork() {
- runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ IntegrationTestUtils.tearDownInfraNetwork(mInfraNetworkTracker);
}
- 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 startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
+ private void startInfraDeviceAndWaitForOnLinkAddr() {
mInfraDevice =
- new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
- mInfraDevice.runSlaac(Duration.ofSeconds(60));
- assertNotNull(mInfraDevice.ipv6Addr);
+ IntegrationTestUtils.startInfraDeviceAndWaitForOnLinkAddr(mInfraNetworkReader);
}
private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
@@ -668,20 +711,28 @@
assertInfraLinkMemberOfGroup(address);
}
- private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
- return pollForPacketOnInfraNetwork(type, srcAddress, null);
+ private byte[] pollForIcmpPacketOnInfraNetwork(int type, InetAddress srcAddress) {
+ return pollForIcmpPacketOnInfraNetwork(type, srcAddress, null /* destAddress */);
}
- private byte[] pollForPacketOnInfraNetwork(
- int type, Inet6Address srcAddress, Inet6Address destAddress) {
- Predicate<byte[]> filter;
- filter =
+ private byte[] pollForIcmpPacketOnInfraNetwork(
+ int type, InetAddress srcAddress, InetAddress destAddress) {
+ if (srcAddress == null && destAddress == null) {
+ throw new IllegalArgumentException("srcAddress and destAddress cannot be both null");
+ }
+ if (srcAddress != null && destAddress != null) {
+ if ((srcAddress instanceof Inet4Address) != (destAddress instanceof Inet4Address)) {
+ throw new IllegalArgumentException(
+ "srcAddress and destAddress must be both IPv4 or both IPv6");
+ }
+ }
+ boolean isIpv4 =
+ (srcAddress instanceof Inet4Address) || (destAddress instanceof Inet4Address);
+ final Predicate<byte[]> filter =
p ->
- (isExpectedIcmpv6Packet(p, type)
- && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
- && (destAddress == null
- ? true
- : isToIpv6Destination(p, destAddress)));
+ (isIpv4 ? isExpectedIcmpv4Packet(p, type) : isExpectedIcmpv6Packet(p, type))
+ && (srcAddress == null || isFrom(p, srcAddress))
+ && (destAddress == null || isTo(p, destAddress));
return pollForPacket(mInfraNetworkReader, filter);
}
}
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index e10f134..2afca5f 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -18,10 +18,10 @@
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
import static android.net.thread.utils.IntegrationTestUtils.resolveService;
import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
@@ -168,8 +168,7 @@
// Creates Full Thread Devices (FTD) and let them join the network.
for (FullThreadDevice ftd : mFtds) {
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
}
int randomId = new Random().nextInt(10_000);
@@ -223,8 +222,7 @@
// Creates a Full Thread Devices (FTD) and let it join the network.
FullThreadDevice ftd = mFtds.get(0);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.setSrpHostname("my-host");
ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
ftd.addSrpService(
@@ -279,8 +277,7 @@
// Creates a Full Thread Devices (FTD) and let it join the network.
FullThreadDevice ftd = mFtds.get(0);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.setSrpHostname("my-host");
ftd.setSrpHostAddresses(
List.of(
@@ -346,8 +343,7 @@
mRegistrationListeners.add(listener);
for (int i = 0; i < NUM_FTD; ++i) {
FullThreadDevice ftd = mFtds.get(i);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
}
final ArrayList<NsdServiceInfo> browsedServices = new ArrayList<>();
@@ -409,8 +405,7 @@
* </pre>
*/
FullThreadDevice srpClient = mFtds.get(0);
- srpClient.joinNetwork(DEFAULT_DATASET);
- srpClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(srpClient, DEFAULT_DATASET);
srpClient.setSrpHostname("my-host");
srpClient.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001::1")));
srpClient.addSrpService(
@@ -421,8 +416,7 @@
Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
FullThreadDevice dnsClient = mFtds.get(1);
- dnsClient.joinNetwork(DEFAULT_DATASET);
- dnsClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(dnsClient, DEFAULT_DATASET);
dnsClient.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
NsdServiceInfo browsedService = dnsClient.browseService("_test._udp.default.service.arpa.");
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
deleted file mode 100644
index ba04348..0000000
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.thread;
-
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
-
-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 org.junit.Assert.assertThrows;
-
-import android.content.Context;
-import android.net.thread.utils.ThreadFeatureCheckerRule;
-import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
-import android.os.OutcomeReceiver;
-import android.util.SparseIntArray;
-
-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.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/** Tests for hide methods of {@link ThreadNetworkController}. */
-@LargeTest
-@RequiresThreadFeature
-@RunWith(AndroidJUnit4.class)
-public class ThreadNetworkControllerTest {
- private static final int VALID_POWER = 32_767;
- private static final int INVALID_POWER = 32_768;
- private static final int VALID_CHANNEL = 20;
- private static final int INVALID_CHANNEL = 10;
- private static final String THREAD_NETWORK_PRIVILEGED =
- "android.permission.THREAD_NETWORK_PRIVILEGED";
-
- private static final SparseIntArray CHANNEL_MAX_POWERS =
- new SparseIntArray() {
- {
- put(20, 32767);
- }
- };
-
- @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
-
- private final Context mContext = ApplicationProvider.getApplicationContext();
- private ExecutorService mExecutor;
- private ThreadNetworkController mController;
-
- @Before
- public void setUp() throws Exception {
- mController =
- mContext.getSystemService(ThreadNetworkManager.class)
- .getAllThreadNetworkControllers()
- .get(0);
-
- mExecutor = Executors.newSingleThreadExecutor();
- }
-
- @After
- public void tearDown() throws Exception {
- dropAllPermissions();
- }
-
- @Test
- public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
- CompletableFuture<Void> powerFuture = new CompletableFuture<>();
-
- runAsShell(
- THREAD_NETWORK_PRIVILEGED,
- () ->
- mController.setChannelMaxPowers(
- CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
-
- try {
- assertThat(powerFuture.get()).isNull();
- } catch (ExecutionException exception) {
- ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_OPERATION);
- }
- }
-
- @Test
- public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
- throws Exception {
- dropAllPermissions();
-
- assertThrows(
- SecurityException.class,
- () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
- }
-
- @Test
- public void setChannelMaxPowers_emptyChannelMaxPower_throwsIllegalArgumentException() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
- }
-
- @Test
- public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
- final SparseIntArray INVALID_CHANNEL_ARRAY =
- new SparseIntArray() {
- {
- put(INVALID_CHANNEL, VALID_POWER);
- }
- };
-
- assertThrows(
- IllegalArgumentException.class,
- () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
- }
-
- @Test
- public void setChannelMaxPowers_invalidPower_throwsIllegalArgumentException() {
- final SparseIntArray INVALID_POWER_ARRAY =
- new SparseIntArray() {
- {
- put(VALID_CHANNEL, INVALID_POWER);
- }
- };
-
- assertThrows(
- IllegalArgumentException.class,
- () -> mController.setChannelMaxPowers(INVALID_POWER_ARRAY, mExecutor, v -> {}));
- }
-
- private static void dropAllPermissions() {
- getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
- }
-
- 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);
- }
- };
- }
-}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index 8835f40..87219d3 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -19,14 +19,18 @@
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import android.content.Context;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
@@ -41,6 +45,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet6Address;
+import java.time.Duration;
+import java.util.List;
import java.util.concurrent.ExecutionException;
/** Integration tests for {@link ThreadNetworkShellCommand}. */
@@ -53,14 +60,24 @@
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
+ private final OtDaemonController mOtCtl = new OtDaemonController();
+ private FullThreadDevice mFtd;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
+ // TODO(b/366141754): The current implementation of "thread_network ot-ctl factoryreset"
+ // results in timeout error.
+ // A future fix will provide proper support for factoryreset, allowing us to replace the
+ // legacy "ot-ctl".
+ mOtCtl.factoryReset();
+
+ mFtd = new FullThreadDevice(10 /* nodeId */);
ensureThreadEnabled();
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
+ mFtd.destroy();
ensureThreadEnabled();
}
@@ -69,6 +86,13 @@
runThreadCommand("enable");
}
+ private static void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(activeDataset);
+ ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
+ }
+
@Test
public void enable_threadStateIsEnabled() throws Exception {
runThreadCommand("enable");
@@ -123,6 +147,38 @@
assertThat(result).contains("Thread country code = CN");
}
+ @Test
+ public void handleOtCtlCommand_enableIfconfig_getIfconfigReturnsUP() {
+ runThreadCommand("ot-ctl ifconfig up");
+
+ final String result = runThreadCommand("ot-ctl ifconfig");
+
+ assertThat(result).isEqualTo("up\r\nDone\r\n");
+ }
+
+ @Test
+ public void handleOtCtlCommand_disableIfconfig_startThreadFailsWithInvalidState() {
+ runThreadCommand("ot-ctl ifconfig down");
+
+ final String result = runThreadCommand("ot-ctl thread start");
+
+ assertThat(result).isEqualTo("Error 13: InvalidState\r\n");
+ }
+
+ @Test
+ public void handleOtCtlCommand_pingFtd_getValidResponse() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ startFtdChild(mFtd, DEFAULT_DATASET);
+ final Inet6Address ftdMlEid = mFtd.getMlEid();
+ assertNotNull(ftdMlEid);
+
+ final String result = runThreadCommand("ot-ctl ping " + ftdMlEid.getHostAddress());
+
+ assertThat(result).contains("1 packets transmitted, 1 packets received");
+ assertThat(result).contains("Packet loss = 0.0%");
+ assertThat(result).endsWith("Done\r\n");
+ }
+
private static String runThreadCommand(String cmd) {
return runShellCommandOrThrow("cmd thread_network " + cmd);
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index c0a8eea..083a841 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -282,6 +282,7 @@
for (String subtype : subtypes) {
fullServiceType.append(",").append(subtype);
}
+ waitForSrpServer();
executeCommand(
"srp client service add %s %s %d %d %d %s",
serviceName,
@@ -416,7 +417,7 @@
executeCommand("ipmaddr add " + address.getHostAddress());
}
- public void ping(Inet6Address address, Inet6Address source) {
+ public void ping(InetAddress address, Inet6Address source) {
ping(
address,
source,
@@ -427,7 +428,7 @@
PING_TIMEOUT_0_1_SECOND);
}
- public void ping(Inet6Address address) {
+ public void ping(InetAddress address) {
ping(
address,
null,
@@ -439,7 +440,7 @@
}
/** Returns the number of ping reply packets received. */
- public int ping(Inet6Address address, int count) {
+ public int ping(InetAddress address, int count) {
List<String> output =
ping(
address,
@@ -453,7 +454,7 @@
}
private List<String> ping(
- Inet6Address address,
+ InetAddress address,
Inet6Address source,
int size,
int count,
@@ -492,6 +493,22 @@
return -1;
}
+ /** Waits for an SRP server to be present in Network Data */
+ private void waitForSrpServer() throws TimeoutException {
+ // CLI output:
+ // > srp client server
+ // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
+ // Done
+ waitFor(
+ () -> {
+ final String serverAddr = executeCommand("srp client server").get(0);
+ final int lastColonIndex = serverAddr.lastIndexOf(':');
+ final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1));
+ return port > 0;
+ },
+ SERVICE_DISCOVERY_TIMEOUT);
+ }
+
@FormatMethod
private List<String> executeCommand(String commandFormat, Object... args) {
return executeCommand(String.format(commandFormat, args));
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
deleted file mode 100644
index ada46c8..0000000
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ /dev/null
@@ -1,474 +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 android.net.thread.utils;
-
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
-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 static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.net.ConnectivityManager;
-import android.net.InetAddresses;
-import android.net.LinkAddress;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.net.TestNetworkInterface;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.net.thread.ThreadNetworkController;
-import android.os.Build;
-import android.os.Handler;
-import android.os.SystemClock;
-
-import androidx.annotation.NonNull;
-import androidx.test.core.app.ApplicationProvider;
-
-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.CompletableFuture;
-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);
- public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
-
- private IntegrationTestUtils() {}
-
- /**
- * 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 = 500;
- 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;
- }
-
- public static List<LinkAddress> getIpv6LinkAddresses(String interfaceName) {
- List<LinkAddress> addresses = new ArrayList<>();
- final String cmd = " ip -6 addr show dev " + interfaceName;
- final String output = runShellCommandOrThrow(cmd);
-
- for (final String line : output.split("\\n")) {
- if (line.contains("inet6")) {
- addresses.add(parseAddressLine(line));
- }
- }
-
- return addresses;
- }
-
- /** Return the first discovered service of {@code serviceType}. */
- public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
- throws Exception {
- CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
- NsdManager.DiscoveryListener listener =
- new DefaultDiscoveryListener() {
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {
- serviceInfoFuture.complete(serviceInfo);
- }
- };
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
- try {
- serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- nsdManager.stopServiceDiscovery(listener);
- }
-
- return serviceInfoFuture.get();
- }
-
- /**
- * Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
- */
- public static NsdManager.DiscoveryListener discoverForServiceLost(
- NsdManager nsdManager,
- String serviceType,
- CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
- NsdManager.DiscoveryListener listener =
- new DefaultDiscoveryListener() {
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {
- serviceInfoFuture.complete(serviceInfo);
- }
- };
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
- return listener;
- }
-
- /** Resolves the service. */
- public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
- throws Exception {
- return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
- }
-
- /** Returns the first resolved service that satisfies the {@code predicate}. */
- public static NsdServiceInfo resolveServiceUntil(
- NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
- throws Exception {
- CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
- NsdManager.ServiceInfoCallback callback =
- new DefaultServiceInfoCallback() {
- @Override
- public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
- if (predicate.test(serviceInfo)) {
- resolvedServiceInfoFuture.complete(serviceInfo);
- }
- }
- };
- nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
- try {
- return resolvedServiceInfoFuture.get(
- SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- nsdManager.unregisterServiceInfoCallback(callback);
- }
- }
-
- public static String getPrefixesFromNetData(String netData) {
- int startIdx = netData.indexOf("Prefixes:");
- int endIdx = netData.indexOf("Routes:");
- return netData.substring(startIdx, endIdx);
- }
-
- public static Network getThreadNetwork(Duration timeout) throws Exception {
- CompletableFuture<Network> networkFuture = new CompletableFuture<>();
- ConnectivityManager cm =
- ApplicationProvider.getApplicationContext()
- .getSystemService(ConnectivityManager.class);
- NetworkRequest.Builder networkRequestBuilder =
- new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD);
- // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
- // a Thread network.
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- networkRequestBuilder.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
- }
- NetworkRequest networkRequest = networkRequestBuilder.build();
- ConnectivityManager.NetworkCallback networkCallback =
- new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(Network network) {
- networkFuture.complete(network);
- }
- };
- cm.registerNetworkCallback(networkRequest, networkCallback);
- return networkFuture.get(timeout.toSeconds(), SECONDS);
- }
-
- private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
- @Override
- public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
-
- @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) {}
- }
-
- private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
- @Override
- public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
-
- @Override
- public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
-
- @Override
- public void onServiceLost() {}
-
- @Override
- public void onServiceInfoCallbackUnregistered() {}
- }
-
- /**
- * Parses a line of output from "ip -6 addr show" into a {@link LinkAddress}.
- *
- * <p>Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
- */
- private static LinkAddress parseAddressLine(String line) {
- String[] parts = line.trim().split("\\s+");
- String addressString = parts[1];
- String[] pieces = addressString.split("/", 2);
- int prefixLength = Integer.parseInt(pieces[1]);
- final InetAddress address = InetAddresses.parseNumericAddress(pieces[0]);
- long deprecationTimeMillis =
- line.contains("deprecated")
- ? SystemClock.elapsedRealtime()
- : LinkAddress.LIFETIME_PERMANENT;
-
- return new LinkAddress(
- address,
- prefixLength,
- 0 /* flags */,
- 0 /* scope */,
- deprecationTimeMillis,
- LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
- }
-}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
new file mode 100644
index 0000000..3df74b0
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -0,0 +1,604 @@
+/*
+ * 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.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.RouteInfo
+import android.net.TestNetworkInterface
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import android.net.thread.ActiveOperationalDataset
+import android.net.thread.ThreadNetworkController
+import android.os.Build
+import android.os.Handler
+import android.os.SystemClock
+import android.system.OsConstants
+import androidx.test.core.app.ApplicationProvider
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.net.module.util.NetworkStackConstants
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv4Header
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv4Header
+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.TapPacketReader
+import com.android.testutils.TestNetworkTracker
+import com.android.testutils.initTestNetwork
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import com.google.common.io.BaseEncoding
+import com.google.common.util.concurrent.MoreExecutors
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import com.google.common.util.concurrent.SettableFuture
+import java.io.IOException
+import java.lang.Byte.toUnsignedInt
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.Inet4Address
+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.concurrent.CompletableFuture
+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
+import org.junit.Assert
+
+/** Utilities for Thread integration tests. */
+object 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
+ @JvmField
+ val RESTART_JOIN_TIMEOUT: Duration = Duration.ofSeconds(40)
+
+ @JvmField
+ val JOIN_TIMEOUT: Duration = Duration.ofSeconds(30)
+
+ @JvmField
+ val LEAVE_TIMEOUT: Duration = Duration.ofSeconds(2)
+
+ @JvmField
+ val CALLBACK_TIMEOUT: Duration = Duration.ofSeconds(1)
+
+ @JvmField
+ val SERVICE_DISCOVERY_TIMEOUT: Duration = Duration.ofSeconds(20)
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private val DEFAULT_DATASET_TLVS: ByteArray = BaseEncoding.base16().decode(
+ ("0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8")
+ )
+
+ @JvmField
+ val DEFAULT_DATASET: ActiveOperationalDataset =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS)
+
+ /**
+ * Waits for the given [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
+ */
+ @JvmStatic
+ @Throws(TimeoutException::class)
+ fun waitFor(condition: Supplier<Boolean>, timeout: Duration) {
+ val intervalMills: Long = 500
+ val timeoutMills = timeout.toMillis()
+
+ var i: Long = 0
+ while (i < timeoutMills) {
+ if (condition.get()) {
+ return
+ }
+ SystemClock.sleep(intervalMills)
+ i += intervalMills
+ }
+ if (condition.get()) {
+ return
+ }
+ throw TimeoutException("The condition failed to become true in $timeout")
+ }
+
+ /**
+ * Creates a [TapPacketReader] given the [TestNetworkInterface] and [Handler].
+ *
+ * @param testNetworkInterface the TUN interface of the test network
+ * @param handler the handler to process the packets
+ * @return the [TapPacketReader]
+ */
+ @JvmStatic
+ fun newPacketReader(
+ testNetworkInterface: TestNetworkInterface, handler: Handler
+ ): TapPacketReader {
+ val fd = testNetworkInterface.fileDescriptor.fileDescriptor
+ val reader = TapPacketReader(handler, fd, testNetworkInterface.mtu)
+ handler.post { reader.start() }
+ handler.waitForIdle(timeoutMs = 5000)
+ return reader
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given `deviceRoles`.
+ *
+ * @param controller the [ThreadNetworkController]
+ * @param deviceRoles the desired device roles. See also [ ]
+ * @param timeout the time to wait for the expected state before throwing
+ * @return the [ThreadNetworkController.DeviceRole] after waiting
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires
+ */
+ @JvmStatic
+ @Throws(TimeoutException::class)
+ fun waitForStateAnyOf(
+ controller: ThreadNetworkController, deviceRoles: List<Int>, timeout: Duration
+ ): Int {
+ val future = SettableFuture.create<Int>()
+ val callback = ThreadNetworkController.StateCallback { newRole: Int ->
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole)
+ }
+ }
+ controller.registerStateCallback(MoreExecutors.directExecutor(), callback)
+ try {
+ return future[timeout.toMillis(), TimeUnit.MILLISECONDS]
+ } catch (e: InterruptedException) {
+ throw TimeoutException(
+ "The device didn't become an expected role in $timeout: $e.message"
+ )
+ } catch (e: ExecutionException) {
+ throw TimeoutException(
+ "The device didn't become an expected role in $timeout: $e.message"
+ )
+ } finally {
+ controller.unregisterStateCallback(callback)
+ }
+ }
+
+ /**
+ * Polls for a packet from a given [TapPacketReader] that satisfies the `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 `filter`. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null
+ */
+ @JvmStatic
+ fun pollForPacket(packetReader: TapPacketReader, filter: Predicate<ByteArray>): ByteArray? {
+ var packet: ByteArray?
+ while ((packetReader.poll(3000 /* timeoutMs */, filter).also { packet = it }) != null) {
+ return packet
+ }
+ return null
+ }
+
+ /** Returns `true` if `packet` is an ICMPv4 packet of given `type`. */
+ @JvmStatic
+ fun isExpectedIcmpv4Packet(packet: ByteArray, type: Int): Boolean {
+ val buf = makeByteBuffer(packet)
+ val header = extractIpv4Header(buf) ?: return false
+ if (header.protocol != OsConstants.IPPROTO_ICMP.toByte()) {
+ return false
+ }
+ try {
+ return Struct.parse(Icmpv4Header::class.java, buf).type == type.toShort()
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false
+ }
+
+ /** Returns `true` if `packet` is an ICMPv6 packet of given `type`. */
+ @JvmStatic
+ fun isExpectedIcmpv6Packet(packet: ByteArray, type: Int): Boolean {
+ val buf = makeByteBuffer(packet)
+ val header = extractIpv6Header(buf) ?: return false
+ if (header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
+ return false
+ }
+ try {
+ return Struct.parse(Icmpv6Header::class.java, buf).type == type.toShort()
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false
+ }
+
+ @JvmStatic
+ fun isFrom(packet: ByteArray, src: InetAddress): Boolean {
+ when (src) {
+ is Inet4Address -> return isFromIpv4Source(packet, src)
+ is Inet6Address -> return isFromIpv6Source(packet, src)
+ else -> return false
+ }
+ }
+
+ @JvmStatic
+ fun isTo(packet: ByteArray, dest: InetAddress): Boolean {
+ when (dest) {
+ is Inet4Address -> return isToIpv4Destination(packet, dest)
+ is Inet6Address -> return isToIpv6Destination(packet, dest)
+ else -> return false
+ }
+ }
+
+ private fun isFromIpv4Source(packet: ByteArray, src: Inet4Address): Boolean {
+ val header = extractIpv4Header(makeByteBuffer(packet))
+ return header?.srcIp == src
+ }
+
+ private fun isFromIpv6Source(packet: ByteArray, src: Inet6Address): Boolean {
+ val header = extractIpv6Header(makeByteBuffer(packet))
+ return header?.srcIp == src
+ }
+
+ private fun isToIpv4Destination(packet: ByteArray, dest: Inet4Address): Boolean {
+ val header = extractIpv4Header(makeByteBuffer(packet))
+ return header?.dstIp == dest
+ }
+
+ private fun isToIpv6Destination(packet: ByteArray, dest: Inet6Address): Boolean {
+ val header = extractIpv6Header(makeByteBuffer(packet))
+ return header?.dstIp == dest
+ }
+
+ private fun makeByteBuffer(packet: ByteArray): ByteBuffer {
+ return ByteBuffer.wrap(packet)
+ }
+
+ private fun extractIpv4Header(buf: ByteBuffer): Ipv4Header? {
+ try {
+ return Struct.parse(Ipv4Header::class.java, buf)
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return null
+ }
+
+ private fun extractIpv6Header(buf: ByteBuffer): Ipv6Header? {
+ try {
+ return Struct.parse(Ipv6Header::class.java, buf)
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return null
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ @JvmStatic
+ fun getRaPios(raMsg: ByteArray?): List<PrefixInformationOption> {
+ val pioList = ArrayList<PrefixInformationOption>()
+
+ raMsg ?: return pioList
+
+ val buf = ByteBuffer.wrap(raMsg)
+ val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+ if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
+ return pioList
+ }
+
+ val icmpv6Header = Struct.parse(Icmpv6Header::class.java, buf)
+ if (icmpv6Header.type != NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT.toShort()) {
+ return pioList
+ }
+
+ Struct.parse(RaHeader::class.java, buf)
+ while (buf.position() < raMsg.size) {
+ val currentPos = buf.position()
+ val type = toUnsignedInt(buf.get())
+ val length = toUnsignedInt(buf.get())
+ if (type == NetworkStackConstants.ICMPV6_ND_OPTION_PIO) {
+ val pioBuf = ByteBuffer.wrap(
+ buf.array(), currentPos, Struct.getSize(PrefixInformationOption::class.java)
+ )
+ val pio = Struct.parse(PrefixInformationOption::class.java, pioBuf)
+ pioList.add(pio)
+
+ // Move ByteBuffer position to the next option.
+ buf.position(
+ currentPos + Struct.getSize(PrefixInformationOption::class.java)
+ )
+ } 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
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun sendUdpMessage(dstAddress: InetAddress, dstPort: Int, message: String) {
+ val dstSockAddr: SocketAddress = InetSocketAddress(dstAddress, dstPort)
+
+ DatagramSocket().use { socket ->
+ socket.connect(dstSockAddr)
+ val msgBytes = message.toByteArray()
+ val packet = DatagramPacket(msgBytes, msgBytes.size)
+ socket.send(packet)
+ }
+ }
+
+ @JvmStatic
+ fun isInMulticastGroup(interfaceName: String, address: Inet6Address): Boolean {
+ val cmd = "ip -6 maddr show dev $interfaceName"
+ val output: String = runShellCommandOrThrow(cmd)
+ val addressStr = address.hostAddress
+ for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ if (line.contains(addressStr)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ @JvmStatic
+ fun getIpv6LinkAddresses(interfaceName: String): List<LinkAddress> {
+ val addresses: MutableList<LinkAddress> = ArrayList()
+ val cmd = " ip -6 addr show dev $interfaceName"
+ val output: String = runShellCommandOrThrow(cmd)
+
+ for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ if (line.contains("inet6")) {
+ addresses.add(parseAddressLine(line))
+ }
+ }
+
+ return addresses
+ }
+
+ /** Return the first discovered service of `serviceType`. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun discoverService(nsdManager: NsdManager, serviceType: String): NsdServiceInfo {
+ val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
+ val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ serviceInfoFuture.complete(serviceInfo)
+ }
+ }
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
+ try {
+ serviceInfoFuture[SERVICE_DISCOVERY_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS]
+ } finally {
+ nsdManager.stopServiceDiscovery(listener)
+ }
+
+ return serviceInfoFuture.get()
+ }
+
+ /**
+ * Returns the [NsdServiceInfo] when a service instance of `serviceType` gets lost.
+ */
+ @JvmStatic
+ fun discoverForServiceLost(
+ nsdManager: NsdManager,
+ serviceType: String?,
+ serviceInfoFuture: CompletableFuture<NsdServiceInfo?>
+ ): NsdManager.DiscoveryListener {
+ val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
+ override fun onServiceLost(serviceInfo: NsdServiceInfo): Unit {
+ serviceInfoFuture.complete(serviceInfo)
+ }
+ }
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
+ return listener
+ }
+
+ /** Resolves the service. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun resolveService(nsdManager: NsdManager, serviceInfo: NsdServiceInfo): NsdServiceInfo {
+ return resolveServiceUntil(nsdManager, serviceInfo) { true }
+ }
+
+ /** Returns the first resolved service that satisfies the `predicate`. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun resolveServiceUntil(
+ nsdManager: NsdManager, serviceInfo: NsdServiceInfo, predicate: Predicate<NsdServiceInfo>
+ ): NsdServiceInfo {
+ val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
+ val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ if (predicate.test(serviceInfo)) {
+ resolvedServiceInfoFuture.complete(serviceInfo)
+ }
+ }
+ }
+ nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback)
+ try {
+ return resolvedServiceInfoFuture[
+ SERVICE_DISCOVERY_TIMEOUT.toMillis(),
+ TimeUnit.MILLISECONDS]
+ } finally {
+ nsdManager.unregisterServiceInfoCallback(callback)
+ }
+ }
+
+ @JvmStatic
+ fun getPrefixesFromNetData(netData: String): String {
+ val startIdx = netData.indexOf("Prefixes:")
+ val endIdx = netData.indexOf("Routes:")
+ return netData.substring(startIdx, endIdx)
+ }
+
+ @JvmStatic
+ @Throws(Exception::class)
+ fun getThreadNetwork(timeout: Duration): Network {
+ val networkFuture = CompletableFuture<Network>()
+ val cm =
+ ApplicationProvider.getApplicationContext<Context>()
+ .getSystemService(ConnectivityManager::class.java)
+ val networkRequestBuilder =
+ NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
+ // a Thread network.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ }
+ val networkRequest = networkRequestBuilder.build()
+ val networkCallback: ConnectivityManager.NetworkCallback =
+ object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ networkFuture.complete(network)
+ }
+ }
+ cm.registerNetworkCallback(networkRequest, networkCallback)
+ return networkFuture[timeout.toSeconds(), TimeUnit.SECONDS]
+ }
+
+ /**
+ * Let the FTD join the specified Thread network and wait for border routing to be available.
+ *
+ * @return the OMR address
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun joinNetworkAndWaitForOmr(
+ ftd: FullThreadDevice, dataset: ActiveOperationalDataset
+ ): Inet6Address {
+ ftd.factoryReset()
+ ftd.joinNetwork(dataset)
+ ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
+ waitFor({ ftd.omrAddress != null }, Duration.ofSeconds(60))
+ Assert.assertNotNull(ftd.omrAddress)
+ return ftd.omrAddress
+ }
+
+ private open class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
+ override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
+ override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
+ override fun onDiscoveryStarted(serviceType: String) {}
+ override fun onDiscoveryStopped(serviceType: String) {}
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {}
+ override fun onServiceLost(serviceInfo: NsdServiceInfo) {}
+ }
+
+ private open class DefaultServiceInfoCallback : NsdManager.ServiceInfoCallback {
+ override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {}
+ override fun onServiceLost(): Unit {}
+ override fun onServiceInfoCallbackUnregistered() {}
+ }
+
+ /**
+ * Parses a line of output from "ip -6 addr show" into a [LinkAddress].
+ *
+ * Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
+ */
+ private fun parseAddressLine(line: String): LinkAddress {
+ val parts = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }.toTypedArray()
+ val addressString = parts[1]
+ val pieces = addressString.split("/".toRegex(), limit = 2).toTypedArray()
+ val prefixLength = pieces[1].toInt()
+ val address = parseNumericAddress(pieces[0])
+ val deprecationTimeMillis =
+ if (line.contains("deprecated")) SystemClock.elapsedRealtime()
+ else LinkAddress.LIFETIME_PERMANENT
+
+ return LinkAddress(
+ address, prefixLength,
+ 0 /* flags */, 0 /* scope */,
+ deprecationTimeMillis, LinkAddress.LIFETIME_PERMANENT /* expirationTime */
+ )
+ }
+
+ private fun defaultLinkProperties(): LinkProperties {
+ val lp = LinkProperties()
+ // TODO: use a fake DNS server
+ lp.setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
+ // NAT64 feature requires the infra network to have an IPv4 default route.
+ lp.addRoute(
+ RouteInfo(
+ IpPrefix("0.0.0.0/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST, 1500 /* mtu */
+ )
+ )
+ return lp
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun startInfraDeviceAndWaitForOnLinkAddr(
+ tapPacketReader: TapPacketReader,
+ macAddress: MacAddress = MacAddress.fromString("1:2:3:4:5:6")
+ ): InfraNetworkDevice {
+ val infraDevice = InfraNetworkDevice(macAddress, tapPacketReader)
+ infraDevice.runSlaac(Duration.ofSeconds(60))
+ requireNotNull(infraDevice.ipv6Addr)
+ return infraDevice
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ @Throws(java.lang.Exception::class)
+ fun setUpInfraNetwork(
+ context: Context,
+ controller: ThreadNetworkControllerWrapper,
+ lp: LinkProperties = defaultLinkProperties()
+ ): TestNetworkTracker {
+ val infraNetworkTracker: TestNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ supplier = { initTestNetwork(context, lp, setupTimeoutMs = 5000) })
+ val infraNetworkName: String = infraNetworkTracker.testIface.getInterfaceName()
+ controller.setTestNetworkAsUpstreamAndWait(infraNetworkName)
+
+ return infraNetworkTracker
+ }
+
+ @JvmStatic
+ fun tearDownInfraNetwork(testNetworkTracker: TestNetworkTracker) {
+ runAsShell(MANAGE_TEST_NETWORKS) { testNetworkTracker.teardown() }
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index b3175fd..046d9bf 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -105,6 +105,35 @@
return prefixes.isEmpty() ? null : prefixes.get(0);
}
+ /** Enables/Disables NAT64 feature. */
+ public void setNat64Enabled(boolean enabled) {
+ executeCommand("nat64 " + (enabled ? "enable" : "disable"));
+ }
+
+ /** Sets the NAT64 CIDR. */
+ public void setNat64Cidr(String cidr) {
+ executeCommand("nat64 cidr " + cidr);
+ }
+
+ /** Returns whether there's a NAT64 prefix in network data */
+ public boolean hasNat64PrefixInNetdata() {
+ // Example (in the 'Routes' section):
+ // fdb2:bae3:5b59:2:0:0::/96 sn low c000
+ List<String> outputLines = executeCommandAndParse("netdata show");
+ for (String line : outputLines) {
+ if (line.contains(" sn")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Adds a prefix in the Network Data. */
+ public void addPrefixInNetworkData(IpPrefix ipPrefix, String flags, String preference) {
+ executeCommand("prefix add " + ipPrefix + " " + flags + " " + preference);
+ executeCommand("netdata register");
+ }
+
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 9404d1b..c6a24ea 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -50,10 +50,10 @@
"service-thread-pre-jarjar",
],
libs: [
- "android.test.base",
- "android.test.runner",
+ "android.test.base.stubs.system",
+ "android.test.runner.stubs.system",
"ServiceConnectivityResources",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
],
jni_libs: [
"libservice-thread-jni",
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
index 2244a89..11c78e3 100644
--- a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -41,6 +41,19 @@
}
@Test
+ public void fromInstant_authoritativeIsSetAsSpecified() {
+ Instant instant = Instant.now();
+
+ OperationalDatasetTimestamp timestampAuthoritativeFalse =
+ OperationalDatasetTimestamp.fromInstant(instant, false);
+ OperationalDatasetTimestamp timestampAuthoritativeTrue =
+ OperationalDatasetTimestamp.fromInstant(instant, true);
+
+ assertThat(timestampAuthoritativeFalse.isAuthoritativeSource()).isEqualTo(false);
+ assertThat(timestampAuthoritativeTrue.isAuthoritativeSource()).isEqualTo(true);
+ }
+
+ @Test
public void fromTlvValue_goodValue_success() {
OperationalDatasetTimestamp timestamp =
OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index ac74372..62801bf 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -17,15 +17,18 @@
package android.net.thread;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
import static android.os.Process.SYSTEM_UID;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
@@ -134,6 +137,16 @@
return (IOperationalDatasetCallback) invocation.getArguments()[0];
}
+ private static IOperationReceiver getActivateEphemeralKeyModeReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
+ private static IOperationReceiver getDeactivateEphemeralKeyModeReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[0];
+ }
+
@Test
public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
setBinderUid(SYSTEM_UID);
@@ -394,7 +407,7 @@
doAnswer(
invoke -> {
getSetChannelMaxPowersReceiver(invoke)
- .onError(ERROR_UNSUPPORTED_OPERATION, "");
+ .onError(ERROR_UNSUPPORTED_FEATURE, "");
return null;
})
.when(mMockService)
@@ -440,4 +453,88 @@
assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(callbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void activateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ Duration lifetime = Duration.ofSeconds(100);
+ doAnswer(
+ invoke -> {
+ getActivateEphemeralKeyModeReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+ mController.activateEphemeralKeyMode(
+ lifetime, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getActivateEphemeralKeyModeReceiver(invoke)
+ .onError(ERROR_FAILED_PRECONDITION, "");
+ return null;
+ })
+ .when(mMockService)
+ .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+ mController.activateEphemeralKeyMode(
+ lifetime,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void deactivateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ doAnswer(
+ invoke -> {
+ getDeactivateEphemeralKeyModeReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+ mController.deactivateEphemeralKeyMode(
+ Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getDeactivateEphemeralKeyModeReceiver(invoke)
+ .onError(ERROR_INTERNAL_ERROR, "");
+ return null;
+ })
+ .when(mMockService)
+ .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+ mController.deactivateEphemeralKeyMode(
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index ae0bc80..d52191a 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -34,6 +34,7 @@
import android.net.DnsResolver;
import android.net.InetAddresses;
+import android.net.LinkProperties;
import android.net.Network;
import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdManager;
@@ -42,6 +43,9 @@
import android.os.Handler;
import android.os.test.TestLooper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
import com.android.server.thread.openthread.INsdResolveHostCallback;
@@ -50,6 +54,7 @@
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;
@@ -57,11 +62,14 @@
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/** Unit tests for {@link NsdPublisher}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
public final class NsdPublisherTest {
private static final DnsTxtAttribute TEST_TXT_ENTRY_1 =
new DnsTxtAttribute("key1", new byte[] {0x01, 0x02});
@@ -578,6 +586,7 @@
verify(mResolveServiceCallback, times(1))
.onServiceResolved(
eq("test-host"),
+ eq(0),
eq("test"),
eq("_test._tcp"),
eq(12345),
@@ -805,7 +814,10 @@
private void prepareTest() {
mTestLooper = new TestLooper();
Handler handler = new Handler(mTestLooper.getLooper());
- mNsdPublisher = new NsdPublisher(mMockNsdManager, mMockDnsResolver, handler);
+ HashMap<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
+ mNsdPublisher =
+ new NsdPublisher(
+ mMockNsdManager, mMockDnsResolver, handler, networkToLinkProperties);
mNsdPublisher.setNetworkForHostResolution(mNetwork);
}
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 2f58943..b97e2b7 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,6 +16,12 @@
package com.android.server.thread;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
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;
@@ -24,9 +30,9 @@
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
@@ -39,6 +45,8 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -49,16 +57,23 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkProvider;
+import android.net.NetworkRequest;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.IOutputReceiver;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkException;
import android.os.Handler;
import android.os.IBinder;
@@ -67,7 +82,6 @@
import android.os.SystemClock;
import android.os.UserManager;
import android.os.test.TestLooper;
-import android.provider.Settings;
import android.util.AtomicFile;
import androidx.test.annotation.UiThreadTest;
@@ -95,9 +109,12 @@
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
-import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
@@ -147,7 +164,6 @@
private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
private static final String TEST_VENDOR_NAME = "test vendor";
private static final String TEST_MODEL_NAME = "test model";
- private static final boolean TEST_VGH_VALUE = false;
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private NetworkAgent mMockNetworkAgent;
@@ -159,6 +175,7 @@
@Mock private IBinder mIBinder;
@Mock Resources mResources;
@Mock ConnectivityResources mConnectivityResources;
+ @Mock Map<Network, LinkProperties> mMockNetworkToLinkProperties;
private Context mContext;
private TestLooper mTestLooper;
@@ -179,6 +196,12 @@
.when(mContext)
.enforceCallingOrSelfPermission(
eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_TESTING), anyString());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(NETWORK_SETTINGS), anyString());
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
@@ -190,8 +213,6 @@
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
@@ -200,8 +221,8 @@
.thenReturn(TEST_VENDOR_OUI);
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
- when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
- .thenReturn(TEST_VGH_VALUE);
+ when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+ .thenReturn(new String[] {});
final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
@@ -220,7 +241,8 @@
mMockNsdPublisher,
mMockUserManager,
mConnectivityResources,
- () -> DEFAULT_COUNTRY_CODE);
+ () -> DEFAULT_COUNTRY_CODE,
+ mMockNetworkToLinkProperties);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@@ -244,8 +266,8 @@
.thenReturn(TEST_VENDOR_OUI);
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
- when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
- .thenReturn(true);
+ when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+ .thenReturn(new String[] {"vt=test"});
mService.initialize();
mTestLooper.dispatchAll();
@@ -255,19 +277,7 @@
assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
assertThat(meshcopTxts.nonStandardTxtEntries)
- .containsExactly(new DnsTxtAttribute("vgh", "1".getBytes(StandardCharsets.UTF_8)));
- }
-
- @Test
- public void getMeshcopTxtAttributes_managedByGoogleIsFalse_vghIsZero() {
- when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
- .thenReturn(false);
-
- MeshcopTxtAttributes meshcopTxts =
- ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
-
- assertThat(meshcopTxts.nonStandardTxtEntries)
- .containsExactly(new DnsTxtAttribute("vgh", "0".getBytes(StandardCharsets.UTF_8)));
+ .containsExactly(new DnsTxtAttribute("vt", "test".getBytes(UTF_8)));
}
@Test
@@ -340,6 +350,61 @@
}
@Test
+ public void makeVendorSpecificTxtAttrs_validTxts_returnsParsedTxtAttrs() {
+ String[] txts = new String[] {"va=123", "vb=", "vc"};
+
+ List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+ assertThat(attrs)
+ .containsExactly(
+ new DnsTxtAttribute("va", "123".getBytes(UTF_8)),
+ new DnsTxtAttribute("vb", new byte[] {}),
+ new DnsTxtAttribute("vc", new byte[] {}));
+ }
+
+ @Test
+ public void makeVendorSpecificTxtAttrs_txtKeyNotStartWithV_throwsIllegalArgument() {
+ String[] txts = new String[] {"abc=123"};
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mService.makeVendorSpecificTxtAttrs(txts));
+ }
+
+ @Test
+ public void makeVendorSpecificTxtAttrs_txtIsTooShort_throwsIllegalArgument() {
+ String[] txtEmptyKey = new String[] {"=123"};
+ String[] txtSingleCharKey = new String[] {"v=456"};
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mService.makeVendorSpecificTxtAttrs(txtEmptyKey));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mService.makeVendorSpecificTxtAttrs(txtSingleCharKey));
+ }
+
+ @Test
+ public void makeVendorSpecificTxtAttrs_txtValueIsEmpty_parseSuccess() {
+ String[] txts = new String[] {"va=", "vb"};
+
+ List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+ assertThat(attrs)
+ .containsExactly(
+ new DnsTxtAttribute("va", new byte[] {}),
+ new DnsTxtAttribute("vb", new byte[] {}));
+ }
+
+ @Test
+ public void makeVendorSpecificTxtAttrs_multipleEquals_splittedByTheFirstEqual() {
+ String[] txts = new String[] {"va=abc=def=123"};
+
+ List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+ assertThat(attrs).containsExactly(new DnsTxtAttribute("va", "abc=def=123".getBytes(UTF_8)));
+ }
+
+ @Test
public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@@ -434,100 +499,6 @@
assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
}
- @Test
- public void airplaneMode_initWithAirplaneModeOn_otDaemonNotStarted() {
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-
- mService.initialize();
- mTestLooper.dispatchAll();
-
- assertThat(mFakeOtDaemon.isInitialized()).isFalse();
- }
-
- @Test
- public void airplaneMode_initWithAirplaneModeOff_threadIsEnabled() {
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-
- mService.initialize();
- mTestLooper.dispatchAll();
-
- assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
- }
-
- @Test
- public void airplaneMode_changesFromOffToOn_stateIsDisabled() {
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
- AtomicReference<BroadcastReceiver> receiverRef =
- captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
- mService.initialize();
- mTestLooper.dispatchAll();
-
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
- receiverRef.get().onReceive(mContext, new Intent());
- mTestLooper.dispatchAll();
-
- assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
- }
-
- @Test
- public void airplaneMode_changesFromOnToOff_stateIsEnabled() {
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
- AtomicReference<BroadcastReceiver> receiverRef =
- captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
- mService.initialize();
- mTestLooper.dispatchAll();
-
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
- receiverRef.get().onReceive(mContext, new Intent());
- mTestLooper.dispatchAll();
-
- assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
- }
-
- @Test
- public void airplaneMode_setEnabledWhenAirplaneModeIsOn_WillNotAutoDisableSecondTime() {
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
- AtomicReference<BroadcastReceiver> receiverRef =
- captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
- CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
- mService.initialize();
-
- mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
- mTestLooper.dispatchAll();
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
- receiverRef.get().onReceive(mContext, new Intent());
- mTestLooper.dispatchAll();
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
- receiverRef.get().onReceive(mContext, new Intent());
- mTestLooper.dispatchAll();
-
- assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
- assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isTrue();
- }
-
- @Test
- public void airplaneMode_setDisabledWhenAirplaneModeIsOn_WillAutoDisableSecondTime() {
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
- AtomicReference<BroadcastReceiver> receiverRef =
- captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
- CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
- mService.initialize();
- mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
- mTestLooper.dispatchAll();
-
- mService.setEnabled(false, newOperationReceiver(setEnabledFuture));
- mTestLooper.dispatchAll();
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
- receiverRef.get().onReceive(mContext, new Intent());
- mTestLooper.dispatchAll();
- Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
- receiverRef.get().onReceive(mContext, new Intent());
- mTestLooper.dispatchAll();
-
- assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
- assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isFalse();
- }
-
private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
@@ -585,6 +556,55 @@
}
@Test
+ public void createRandomizedDataset_zeroNanoseconds_returnsZeroTicks() throws Exception {
+ Instant now = Instant.ofEpochSecond(0, 0);
+ Clock clock = Clock.fixed(now, ZoneId.systemDefault());
+ MockitoSession session =
+ ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+ try {
+ ExtendedMockito.when(SystemClock.currentNetworkTimeClock()).thenReturn(clock);
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+ } finally {
+ session.finishMocking();
+ }
+
+ verify(mockReceiver, never()).onError(anyInt(), anyString());
+ verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+ ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+ assertThat(activeDataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+ }
+
+ @Test
+ public void createRandomizedDataset_maxNanoseconds_returnsMaxTicks() throws Exception {
+ // The nanoseconds to ticks conversion is rounded in the current implementation.
+ // 32767.5 / 32768 * 1000000000 = 999984741.2109375, using 999984741 to
+ // produce the maximum ticks.
+ Instant now = Instant.ofEpochSecond(0, 999984741);
+ Clock clock = Clock.fixed(now, ZoneId.systemDefault());
+ MockitoSession session =
+ ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+ try {
+ ExtendedMockito.when(SystemClock.currentNetworkTimeClock()).thenReturn(clock);
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+ } finally {
+ session.finishMocking();
+ }
+
+ verify(mockReceiver, never()).onError(anyInt(), anyString());
+ verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+ ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+ assertThat(activeDataset.getActiveTimestamp().getTicks()).isEqualTo(32767);
+ }
+
+ @Test
public void createRandomizedDataset_hasNetworkTimeClock_datasetActiveTimestampIsAuthoritative()
throws Exception {
MockitoSession session =
@@ -708,4 +728,136 @@
inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(false);
inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(true);
}
+
+ @Test
+ public void setConfiguration_configurationUpdated() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+ final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+ final IOperationReceiver mockReceiver3 = mock(IOperationReceiver.class);
+ ThreadConfiguration config1 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .setDhcpv6PdEnabled(false)
+ .build();
+ ThreadConfiguration config2 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcpv6PdEnabled(true)
+ .build();
+ ThreadConfiguration config3 =
+ new ThreadConfiguration.Builder(config2).build(); // Same as config2
+
+ mService.setConfiguration(config1, mockReceiver1);
+ mService.setConfiguration(config2, mockReceiver2);
+ mService.setConfiguration(config3, mockReceiver3);
+ mTestLooper.dispatchAll();
+
+ assertThat(mPersistentSettings.getConfiguration()).isEqualTo(config3);
+ InOrder inOrder = Mockito.inOrder(mockReceiver1, mockReceiver2, mockReceiver3);
+ inOrder.verify(mockReceiver1).onSuccess();
+ inOrder.verify(mockReceiver2).onSuccess();
+ inOrder.verify(mockReceiver3).onSuccess();
+ }
+
+ @Test
+ public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+ ArgumentCaptor.forClass(NetworkRequest.class);
+ verify(mMockConnectivityManager, atLeastOnce())
+ .registerNetworkCallback(
+ networkRequestCaptor.capture(),
+ any(ConnectivityManager.NetworkCallback.class),
+ any(Handler.class));
+ List<NetworkRequest> upstreamNetworkRequests =
+ networkRequestCaptor.getAllValues().stream()
+ .filter(nr -> !nr.hasTransport(TRANSPORT_THREAD))
+ .toList();
+ assertThat(upstreamNetworkRequests.size()).isEqualTo(1);
+ NetworkRequest upstreamNetworkRequest = upstreamNetworkRequests.get(0);
+ assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_WIFI)).isTrue();
+ assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_ETHERNET)).isTrue();
+ assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+ assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_INTERNET)).isTrue();
+ }
+
+ @Test
+ public void setTestNetworkAsUpstream_upstreamNetworkRequestAlwaysDisallowsVpn() {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockConnectivityManager);
+
+ final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+ final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+ mService.setTestNetworkAsUpstream("test-network", mockReceiver1);
+ mService.setTestNetworkAsUpstream(null, mockReceiver2);
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+ ArgumentCaptor.forClass(NetworkRequest.class);
+ verify(mMockConnectivityManager, times(2))
+ .registerNetworkCallback(
+ networkRequestCaptor.capture(),
+ any(ConnectivityManager.NetworkCallback.class),
+ any(Handler.class));
+ assertThat(networkRequestCaptor.getAllValues().size()).isEqualTo(2);
+ NetworkRequest networkRequest1 = networkRequestCaptor.getAllValues().get(0);
+ NetworkRequest networkRequest2 = networkRequestCaptor.getAllValues().get(1);
+ assertThat(networkRequest1.getNetworkSpecifier()).isNotNull();
+ assertThat(networkRequest1.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+ assertThat(networkRequest2.getNetworkSpecifier()).isNull();
+ assertThat(networkRequest2.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+ }
+
+ @Test
+ public void runOtCtlCommand_noPermission_throwsSecurityException() {
+ doThrow(new SecurityException(""))
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), any());
+ doThrow(new SecurityException(""))
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_TESTING), any());
+
+ assertThrows(
+ SecurityException.class,
+ () -> mService.runOtCtlCommand("", false, new IOutputReceiver.Default()));
+ }
+
+ @Test
+ public void runOtCtlCommand_otDaemonRemoteFailure_receiverOnErrorIsCalled() throws Exception {
+ mService.initialize();
+ final IOutputReceiver mockReceiver = mock(IOutputReceiver.class);
+ mFakeOtDaemon.setRunOtCtlCommandException(
+ new RemoteException("ot-daemon runOtCtlCommand() throws"));
+
+ mService.runOtCtlCommand("ot-ctl state", false, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+
+ @Test
+ public void activateEphemeralKeyMode_succeed() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.activateEphemeralKeyMode(1_000L, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void deactivateEphemeralKeyMode_succeed() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.deactivateEphemeralKeyMode(mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
index dfb3129..af5c9aa 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -20,12 +20,15 @@
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.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -35,8 +38,10 @@
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IOutputReceiver;
import android.net.thread.PendingOperationalDataset;
import android.os.Binder;
+import android.os.Process;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
@@ -47,6 +52,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -95,6 +101,9 @@
mShellCommand = new ThreadNetworkShellCommand(mContext, mControllerService, mCountryCode);
mShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+
+ // by default emulate shell uid.
+ BinderUtil.setUid(Process.SHELL_UID);
}
@After
@@ -102,16 +111,20 @@
validateMockitoUsage();
}
- @Test
- public void getCountryCode_testingPermissionIsChecked() {
- when(mCountryCode.getCountryCode()).thenReturn("US");
-
+ private void runShellCommand(String... args) {
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
- new String[] {"get-country-code"});
+ args);
+ }
+
+ @Test
+ public void getCountryCode_testingPermissionIsChecked() {
+ when(mCountryCode.getCountryCode()).thenReturn("US");
+
+ runShellCommand("get-country-code");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
@@ -122,24 +135,14 @@
public void getCountryCode_currentCountryCodePrinted() {
when(mCountryCode.getCountryCode()).thenReturn("US");
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"get-country-code"});
+ runShellCommand("get-country-code");
verify(mOutputWriter).println(contains("US"));
}
@Test
public void forceSetCountryCodeEnabled_testingPermissionIsChecked() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-country-code", "enabled", "US"});
+ runShellCommand("force-country-code", "enabled", "US");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
@@ -148,36 +151,21 @@
@Test
public void forceSetCountryCodeEnabled_countryCodeIsOverridden() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-country-code", "enabled", "US"});
+ runShellCommand("force-country-code", "enabled", "US");
verify(mCountryCode).setOverrideCountryCode(eq("US"));
}
@Test
public void forceSetCountryCodeDisabled_overriddenCountryCodeIsCleared() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-country-code", "disabled"});
+ runShellCommand("force-country-code", "disabled");
verify(mCountryCode).clearOverrideCountryCode();
}
@Test
public void forceStopOtDaemon_testingPermissionIsChecked() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-stop-ot-daemon", "enabled"});
+ runShellCommand("force-stop-ot-daemon", "enabled");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
@@ -190,12 +178,7 @@
.when(mControllerService)
.forceStopOtDaemonForTest(eq(true), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-stop-ot-daemon", "enabled"});
+ runShellCommand("force-stop-ot-daemon", "enabled");
verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
verify(mOutputWriter, never()).println();
@@ -205,12 +188,7 @@
public void forceStopOtDaemon_serviceApiTimeout_failedWithTimeoutError() {
doNothing().when(mControllerService).forceStopOtDaemonForTest(eq(true), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-stop-ot-daemon", "enabled"});
+ runShellCommand("force-stop-ot-daemon", "enabled");
verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
@@ -221,12 +199,7 @@
public void join_controllerServiceJoinIsCalled() {
doNothing().when(mControllerService).join(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"join", DEFAULT_ACTIVE_DATASET_TLVS});
+ runShellCommand("join", DEFAULT_ACTIVE_DATASET_TLVS);
var activeDataset =
ActiveOperationalDataset.fromThreadTlvs(
@@ -239,12 +212,7 @@
public void join_invalidDataset_controllerServiceJoinIsNotCalled() {
doNothing().when(mControllerService).join(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"join", "000102"});
+ runShellCommand("join", "000102");
verify(mControllerService, never()).join(any(), any());
verify(mErrorWriter, times(1)).println(contains("Invalid dataset argument"));
@@ -254,12 +222,7 @@
public void migrate_controllerServiceMigrateIsCalled() {
doNothing().when(mControllerService).scheduleMigration(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"migrate", DEFAULT_ACTIVE_DATASET_TLVS, "300"});
+ runShellCommand("migrate", DEFAULT_ACTIVE_DATASET_TLVS, "300");
ArgumentCaptor<PendingOperationalDataset> captor =
ArgumentCaptor.forClass(PendingOperationalDataset.class);
@@ -276,12 +239,7 @@
public void migrate_invalidDataset_controllerServiceMigrateIsNotCalled() {
doNothing().when(mControllerService).scheduleMigration(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"migrate", "000102", "300"});
+ runShellCommand("migrate", "000102", "300");
verify(mControllerService, never()).scheduleMigration(any(), any());
verify(mErrorWriter, times(1)).println(contains("Invalid dataset argument"));
@@ -291,14 +249,75 @@
public void leave_controllerServiceLeaveIsCalled() {
doNothing().when(mControllerService).leave(any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"leave"});
+ runShellCommand("leave");
verify(mControllerService, times(1)).leave(any());
verify(mErrorWriter, never()).println();
}
+
+ @Test
+ public void handleOtCtlCommand_testingPermissionIsChecked() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doAnswer(
+ invocation -> {
+ IOutputReceiver receiver = invocation.getArgument(1);
+ receiver.onComplete();
+ return null;
+ })
+ .when(mControllerService)
+ .runOtCtlCommand(anyString(), anyBoolean(), any());
+
+ runShellCommand("ot-ctl", "state");
+
+ verify(mContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+ }
+
+ @Test
+ public void handleOtCtlCommand_failsWithNonRootProcess() {
+ runShellCommand("ot-ctl", "state");
+
+ verify(mErrorWriter, times(1)).println(contains("No access to ot-ctl command"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void handleOtCtlCommand_nonInteractive_serviceTimeout_failsWithTimeoutError() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doNothing().when(mControllerService).runOtCtlCommand(anyString(), eq(false), any());
+
+ runShellCommand("ot-ctl", "state");
+
+ verify(mControllerService, times(1)).runOtCtlCommand(anyString(), eq(false), any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void handleOtCtlCommand_nonInteractive_state_outputIsPrinted() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doAnswer(
+ invocation -> {
+ IOutputReceiver receiver = invocation.getArgument(2);
+
+ receiver.onOutput("leader");
+ receiver.onOutput("\r\n");
+ receiver.onOutput("Done");
+ receiver.onOutput("\r\n");
+
+ receiver.onComplete();
+ return null;
+ })
+ .when(mControllerService)
+ .runOtCtlCommand(eq("state"), eq(false), any());
+
+ runShellCommand("ot-ctl", "state");
+
+ InOrder inOrder = inOrder(mOutputWriter);
+ inOrder.verify(mOutputWriter).print("leader");
+ inOrder.verify(mOutputWriter).print("\r\n");
+ inOrder.verify(mOutputWriter).print("Done");
+ inOrder.verify(mOutputWriter).print("\r\n");
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index 7d2fe91..ba489d9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -21,16 +21,12 @@
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.net.thread.ThreadConfiguration;
import android.os.PersistableBundle;
import android.util.AtomicFile;
@@ -42,13 +38,14 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
import java.io.FileOutputStream;
/** Unit tests for {@link ThreadPersistentSettings}. */
@@ -57,12 +54,15 @@
public class ThreadPersistentSettingsTest {
private static final String TEST_COUNTRY_CODE = "CN";
- @Mock private AtomicFile mAtomicFile;
@Mock Resources mResources;
@Mock ConnectivityResources mConnectivityResources;
+ private AtomicFile mAtomicFile;
private ThreadPersistentSettings mThreadPersistentSettings;
+ @Rule(order = 0)
+ public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -70,8 +70,7 @@
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);
+ mAtomicFile = createAtomicFile();
mThreadPersistentSettings =
new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
}
@@ -85,7 +84,7 @@
@Test
public void initialize_readsFromFile() throws Exception {
byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
- setupAtomicFileMockForRead(data);
+ setupAtomicFileForRead(data);
mThreadPersistentSettings.initialize();
@@ -95,7 +94,7 @@
@Test
public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
- setupAtomicFileMockForRead(new byte[0]);
+ setupAtomicFileForRead(new byte[0]);
mThreadPersistentSettings.initialize();
@@ -107,7 +106,7 @@
throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
- setupAtomicFileMockForRead(data);
+ setupAtomicFileForRead(data);
mThreadPersistentSettings.initialize();
@@ -119,9 +118,6 @@
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
@@ -129,9 +125,8 @@
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());
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
}
@Test
@@ -139,10 +134,8 @@
mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, TEST_COUNTRY_CODE);
assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
-
- // Confirm that file writes have been triggered.
- verify(mAtomicFile).startWrite();
- verify(mAtomicFile).finishWrite(any());
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
}
@Test
@@ -150,10 +143,63 @@
mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, null);
assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+ }
- // Confirm that file writes have been triggered.
- verify(mAtomicFile).startWrite();
- verify(mAtomicFile).finishWrite(any());
+ @Test
+ public void putConfiguration_sameValues_returnsFalse() {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcpv6PdEnabled(true)
+ .build();
+ mThreadPersistentSettings.putConfiguration(configuration);
+
+ assertThat(mThreadPersistentSettings.putConfiguration(configuration)).isFalse();
+ }
+
+ @Test
+ public void putConfiguration_differentValues_returnsTrue() {
+ ThreadConfiguration configuration1 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .setDhcpv6PdEnabled(false)
+ .build();
+ mThreadPersistentSettings.putConfiguration(configuration1);
+ ThreadConfiguration configuration2 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcpv6PdEnabled(true)
+ .build();
+
+ assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
+ }
+
+ @Test
+ public void putConfiguration_nat64Enabled_valuesUpdatedAndPersisted() throws Exception {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ mThreadPersistentSettings.putConfiguration(configuration);
+
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ }
+
+ @Test
+ public void putConfiguration_dhcpv6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder().setDhcpv6PdEnabled(true).build();
+ mThreadPersistentSettings.putConfiguration(configuration);
+
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ }
+
+ private AtomicFile createAtomicFile() throws Exception {
+ return new AtomicFile(mTemporaryFolder.newFile());
}
private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
@@ -164,19 +210,9 @@
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());
+ private void setupAtomicFileForRead(byte[] dataToRead) throws Exception {
+ try (FileOutputStream outputStream = new FileOutputStream(mAtomicFile.getBaseFile())) {
+ outputStream.write(dataToRead);
+ }
}
}