Merge history of packages/Connectivity
Modified Visibility / Includes for:
- Tethering/common/TetheringLib/Android.bp
Modified License Import for:
- framework/Android.bp
- service/Android.bp
- tests/common/Android.bp
- tests/deflake/Android.bp
- tests/integration/Android.bp
- tests/smoketest/Android.bp
- tests/unit/Android.bp
- tests/unit/jni/Android.bp
Modified Lint mapping for:
- framework/lint-baseline.xml
- service/lint-baseline.xml
BUG: 186628461
TEST: TH
Merged-In: Ie82d0fb34bda77543e31c82660c6f315efa87f62
Change-Id: I3c2563d4ae4e3715d0c6270344ba8f7ef067872f
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ccff052
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# Eclipse project
+**/.classpath
+**/.project
+
+# IntelliJ project
+**/.idea
+**/*.iml
+**/*.ipr
diff --git a/OWNERS b/OWNERS
index 48e54da..22b5561 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,7 @@
-set noparent
-
-include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
+codewiz@google.com
+jchalard@google.com
+junyulai@google.com
+lorenzo@google.com
+maze@google.com
+reminv@google.com
+satk@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..ebc1264
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,4 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 94f9232..d7d4bcb 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,4 +1,37 @@
{
+ "presubmit": [
+ // 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": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
+ }
+ ],
+ "mainline-presubmit": [
+ {
+ // TODO: add back the tethering modules when updatable in this branch
+ "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex]",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
+ }
+ ],
+ // Tests on physical devices with SIM cards: postsubmit only for capacity constraints
+ "mainline-postsubmit": [
+ {
+ // TODO: add back the tethering module when updatable in this branch
+ "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex]",
+ "keywords": ["sim"]
+ }
+ ],
"imports": [
{
"path": "frameworks/base/core/java/android/net"
@@ -16,4 +49,4 @@
"path": "packages/modules/Connectivity/Tethering"
}
]
-}
\ No newline at end of file
+}
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
new file mode 100644
index 0000000..f469d89
--- /dev/null
+++ b/Tethering/Android.bp
@@ -0,0 +1,164 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "TetheringAndroidLibraryDefaults",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ srcs: [
+ "apishim/**/*.java",
+ "src/**/*.java",
+ ":framework-connectivity-shared-srcs",
+ ":tethering-module-utils-srcs",
+ ":services-tethering-shared-srcs",
+ ],
+ static_libs: [
+ "NetworkStackApiStableShims",
+ "androidx.annotation_annotation",
+ "modules-utils-build",
+ "netlink-client",
+ "networkstack-client",
+ "android.hardware.tetheroffload.config-V1.0-java",
+ "android.hardware.tetheroffload.control-V1.0-java",
+ "android.hardware.tetheroffload.control-V1.1-java",
+ "net-utils-framework-common",
+ "net-utils-device-common",
+ "netd-client",
+ "NetworkStackApiCurrentShims",
+ ],
+ libs: [
+ "framework-connectivity",
+ "framework-statsd.stubs.module_lib",
+ "framework-tethering.impl",
+ "framework-wifi",
+ "unsupportedappusage",
+ ],
+ plugins: ["java_api_finder"],
+ manifest: "AndroidManifestBase.xml",
+}
+
+// Build tethering static library, used to compile both variants of the tethering.
+android_library {
+ name: "TetheringApiCurrentLib",
+ defaults: ["TetheringAndroidLibraryDefaults"],
+}
+
+// Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK).
+cc_library {
+ name: "libtetherutilsjni",
+ sdk_version: "current",
+ apex_available: [
+ "//apex_available:platform", // Used by InProcessTethering
+ "com.android.tethering",
+ ],
+ min_sdk_version: "30",
+ header_libs: [
+ "bpf_syscall_wrappers",
+ "bpf_tethering_headers",
+ ],
+ srcs: [
+ "jni/*.cpp",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper_compat_libc++",
+ ],
+ static_libs: [
+ "libnetjniutils",
+ ],
+
+ // We cannot use plain "libc++" here to link libc++ dynamically because it results in:
+ // java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found
+ // even if "libc++" is added into jni_libs below. Adding "libc++_shared" into jni_libs doesn't
+ // build because soong complains of:
+ // module Tethering missing dependencies: libc++_shared
+ //
+ // So, link libc++ statically. This means that we also need to ensure that all the C++ libraries
+ // we depend on do not dynamically link libc++. This is currently the case, because liblog is
+ // C-only and libnativehelper_compat_libc also uses stl: "c++_static".
+ stl: "c++_static",
+
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+
+ ldflags: ["-Wl,--exclude-libs=ALL,-error-limit=0"],
+}
+
+// Common defaults for compiling the actual APK.
+java_defaults {
+ name: "TetheringAppDefaults",
+ sdk_version: "module_current",
+ privileged: true,
+ jni_libs: [
+ "libtetherutilsjni",
+ ],
+ resource_dirs: [
+ "res",
+ ],
+ libs: [
+ "framework-tethering",
+ "framework-wifi",
+ ],
+ jarjar_rules: "jarjar-rules.txt",
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
+}
+
+// Non-updatable tethering running in the system server process for devices not using the module
+android_app {
+ name: "InProcessTethering",
+ defaults: ["TetheringAppDefaults"],
+ static_libs: ["TetheringApiCurrentLib"],
+ certificate: "platform",
+ manifest: "AndroidManifest_InProcess.xml",
+ // InProcessTethering is a replacement for Tethering
+ overrides: ["Tethering"],
+ apex_available: ["com.android.tethering"],
+ min_sdk_version: "30",
+}
+
+// Updatable tethering packaged as an application
+android_app {
+ name: "Tethering",
+ defaults: ["TetheringAppDefaults"],
+ static_libs: ["TetheringApiCurrentLib"],
+ certificate: "networkstack",
+ manifest: "AndroidManifest.xml",
+ use_embedded_native_libs: true,
+ // The permission configuration *must* be included to ensure security of the device
+ required: [
+ "NetworkPermissionConfig",
+ "privapp_whitelist_com.android.networkstack.tethering",
+ ],
+ apex_available: ["com.android.tethering"],
+ min_sdk_version: "30",
+}
+
+sdk {
+ name: "tethering-module-sdk",
+ java_sdk_libs: [
+ "framework-tethering",
+ ],
+}
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
new file mode 100644
index 0000000..e6444f3
--- /dev/null
+++ b/Tethering/AndroidManifest.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.networkstack.tethering"
+ android:sharedUserId="android.uid.networkstack">
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+
+ <!-- Permissions must be defined here, and not in the base manifest, as the tethering
+ running in the system server process does not need any permission, and having
+ privileged permissions added would cause crashes on startup unless they are also
+ added to the privileged permissions allowlist for that package. -->
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
+ <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.MANAGE_USB" />
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+ <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.TETHER_PRIVILEGED" />
+ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+ <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+ <protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
+
+ <application
+ android:process="com.android.networkstack.process"
+ android:extractNativeLibs="false"
+ android:persistent="true">
+ <service android:name="com.android.networkstack.tethering.TetheringService"
+ android:permission="android.permission.MAINLINE_NETWORK_STACK"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.net.ITetheringConnector"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/Tethering/AndroidManifestBase.xml b/Tethering/AndroidManifestBase.xml
new file mode 100644
index 0000000..97c3988
--- /dev/null
+++ b/Tethering/AndroidManifestBase.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.networkstack.tethering"
+ android:versionCode="1"
+ android:versionName="R-initial">
+ <application
+ android:label="Tethering"
+ android:defaultToDeviceProtectedStorage="true"
+ android:directBootAware="true"
+ android:usesCleartextTraffic="true">
+ </application>
+</manifest>
diff --git a/Tethering/AndroidManifest_InProcess.xml b/Tethering/AndroidManifest_InProcess.xml
new file mode 100644
index 0000000..b1f1240
--- /dev/null
+++ b/Tethering/AndroidManifest_InProcess.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.networkstack.tethering.inprocess"
+ android:sharedUserId="android.uid.system"
+ android:process="system">
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+ <application>
+ <service android:name="com.android.networkstack.tethering.TetheringService"
+ android:process="system"
+ android:permission="android.permission.MAINLINE_NETWORK_STACK"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.net.ITetheringConnector.InProcess"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/Tethering/OWNERS b/Tethering/OWNERS
new file mode 100644
index 0000000..5b42d49
--- /dev/null
+++ b/Tethering/OWNERS
@@ -0,0 +1,2 @@
+include platform/packages/modules/NetworkStack/:/OWNERS
+markchien@google.com
diff --git a/Tethering/TEST_MAPPING b/Tethering/TEST_MAPPING
new file mode 100644
index 0000000..5617b0c
--- /dev/null
+++ b/Tethering/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+ "presubmit": [
+ {
+ "name": "TetheringTests"
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "TetheringIntegrationTests"
+ }
+ ]
+}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
new file mode 100644
index 0000000..dcdf693
--- /dev/null
+++ b/Tethering/apex/Android.bp
@@ -0,0 +1,83 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+apex {
+ name: "com.android.tethering",
+ compile_multilib: "both",
+ updatable: true,
+ min_sdk_version: "30",
+ bootclasspath_fragments: [
+ "com.android.tethering-bootclasspath-fragment",
+ ],
+ java_libs: [
+ "service-connectivity",
+ ],
+ multilib: {
+ first: {
+ jni_libs: ["libservice-connectivity"]
+ },
+ both: {
+ jni_libs: ["libframework-connectivity-jni"],
+ }
+ },
+ bpfs: [
+ "offload.o",
+ "test.o",
+ ],
+ apps: [
+ "ServiceConnectivityResources",
+ "Tethering",
+ ],
+ manifest: "manifest.json",
+ key: "com.android.tethering.key",
+
+ androidManifest: "AndroidManifest.xml",
+}
+
+apex_key {
+ name: "com.android.tethering.key",
+ public_key: "com.android.tethering.avbpubkey",
+ private_key: "com.android.tethering.pem",
+}
+
+android_app_certificate {
+ name: "com.android.tethering.certificate",
+ certificate: "com.android.tethering",
+}
+
+// Encapsulate the contributions made by the com.android.tethering to the bootclasspath.
+bootclasspath_fragment {
+ name: "com.android.tethering-bootclasspath-fragment",
+ contents: [
+ "framework-connectivity",
+ "framework-tethering",
+ ],
+ apex_available: ["com.android.tethering"],
+}
+
+override_apex {
+ name: "com.android.tethering.inprocess",
+ base: "com.android.tethering",
+ package_name: "com.android.tethering.inprocess",
+ apps: [
+ "ServiceConnectivityResources",
+ "InProcessTethering",
+ ],
+}
diff --git a/Tethering/apex/AndroidManifest.xml b/Tethering/apex/AndroidManifest.xml
new file mode 100644
index 0000000..4aae3cc
--- /dev/null
+++ b/Tethering/apex/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tethering">
+ <!-- APEX does not have classes.dex -->
+ <application android:hasCode="false" />
+ <!-- b/145383354: Current minSdk is locked to Q for development cycle, lock it to next version
+ before ship. -->
+ <!-- TODO: Uncomment this when the R API level is fixed. b/148281152 -->
+ <!--uses-sdk
+ android:minSdkVersion="29"
+ android:targetSdkVersion="29"
+ />
+ -->
+</manifest>
diff --git a/Tethering/apex/com.android.tethering.avbpubkey b/Tethering/apex/com.android.tethering.avbpubkey
new file mode 100644
index 0000000..9a2c017
--- /dev/null
+++ b/Tethering/apex/com.android.tethering.avbpubkey
Binary files differ
diff --git a/Tethering/apex/com.android.tethering.pem b/Tethering/apex/com.android.tethering.pem
new file mode 100644
index 0000000..d4f39ab
--- /dev/null
+++ b/Tethering/apex/com.android.tethering.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKgIBAAKCAgEA+AWTp03PBRMGt4mVNLt5PDoFFSfmFOVTM7jt5AJXnQMIDsAM
+1cyWGWRridGIpoHAaCALVgW5aRySgi8yV5xP4w0YHcKbfh9M6I9oz4RUo4GQBZfX
++lFIGaLjb6I3tEJxPuxps4sW26Io63ihwTnKeGyADHdHGWDUs9WU0Ml+QTvKrdjy
+qC03M0dehYXILGiA9m+UXwKoKxhWgfDUhWLhDBUtLJLPL4WeqKc9sG9h+zzVqE+8
+LzJsfrodKhTTrLpWOXi6YLRTk8dzsuPz/Nu98sJd1w3fHd20DrmkqsxVhgN1h+nk
+zcPpxyGYIP6qYVZCmIXCwZZNtPeb7y/tOs967VHoZ4Qj7p2tE0CAWFMZFGjA/pcZ
+7fi6CsIuMOYBbj4+wRlJwpG1g5zSJBCjzhv7dZp8S5oXmLShNYOMYEdsPfaZbm08
+3pVY+k8DVf7idcANXNw1lM+sPbE2hp5VuEuVpK+ca5x8hIMpTqJ84wDAjnC1kCwm
+X2xfNvYPKNF58SvqlNCPN8X7hQjoeaEb7w24vCdZMRqeGBmu1GNQvCyzbBO0huQm
+f5CQPrZjPcnoImlP879VPxY4YB6tAjsA/ZLiub9VdT108lCjb5r8criMzpMAA/AQ
+NqQLWFI3M43xPemGBTiIguTYgpRgGcdRZf7XuTgTY5qzQZZuZMVuwaqSD2cCAwEA
+AQKCAgEA0jMvw3BPTrakT7Lb8JgelKt7mUV6WyVMUZ6eh0pw5JIoJxAfEKfWYmjY
+NzKNRMjcv6LA2MP7MplTld/YI6ZHkl+Lm9VOISL39HVuV8mIThbFb+gT1INEvu1t
+IjRyT2SsQ67rmo377mLNmVtgg7mt3kfecjI44MpPGqad/CF4zmKVUKd4aI4BpYUM
+F8+dKf3bpoBEWA2RZwy2bGQmSXHW132vDoLR8y2knL04rCqJ+PrC/WWuULXEe9bS
+VtLV3yMBZq3qD4Fk/+7fILLPGvNFVdPi4htQiChYrM4rP9HzfaO63VieYMF0hR70
+pqoOznXj9Q4QVC9FZmUgFCQjQ1+KhqJw3OldIo0SnvpsLdTO/inKkhQWKC5HlPyh
+/rqvro2j3pTHWPAziuBr+oQPcdVCOlCBZ+B99L1tO7aGktVPEIVQG7G7jlFMBiJ1
+j/kRGk2RTX8RaPQJTnwUqp8mWUV2fwxHiXNadjejA5ZU3eQT2eAOhXl1w6Lv2jEl
+0wMOwPMJGcF77CcqnnWHON8fkxCbAfyy5Uo6Pm9g/Zzecn+ji2sabG7Ge5t0gzdL
+LKRcGoyakN2CrbQ8pxlCTgE4HX5oPY+VuqOf8L3AIWIJBsyLbXHVkL1mqQ/Ed2uz
+zaaSFYUZw81+m/5bl8JLPaIFNPyikZrXTD0YRer3V06XiyP/kYECggEBAP033xeF
+OhgRwkRTjd68hwRJpyHsZDWxHiUqQf6l6yFv5mEE355G2IGI7cZmR2+tUDjQdxLv
+tAZIszTK4PFCdVTeWfGVFbVF84eNWLB124pHDMM79GN/AMcuHnQPR756a8IO1hIy
+4KxIUE1a1PKN5b9IgE5Lu4TZM96HDpFcUAmCT5urdYDmg3++IWT9PYQlGS7Hhiar
+r+Hh646waM8Qx619CwXBqy+Y37+WHVbYqJClr6AcpVMrGA+6cgpskFpZAPLsoy7G
+RSsVfyV8pH2JKm/hzk7XCwIpczxeWQSfpJWZ+oOPFHu+zM60Cdj2UrQyKrNHwew8
++WYe9eCA+MiNBcECggEBAPq/F1vdqROiLv9uzhKb8ybgdL7CmREELiqwK+MvNE9t
+W7lQz7lcWzav+b2n0M+VJBxUWB3XClgoIvA/AllgTgsYXfKAxNakhKLSBoMmvKCW
+HtWcGr/D3RcmacK+DTMWlVS/LuueAFLuH6UmBIUFKc+qA5x7oQecAFALBFupE3G4
+LtAspLBI6P8gRtRav5p2whs9H8qjYcyf2f6liWpkmFITcXvPvAxFHicR6ZJdwZ/S
+PiX2LJQnOpT7L3+2PWnYwzFStb4MkMGlFKcscU9CvS53JcP/J4Asjk0I4zDB2gri
+xzFHPlVzCr2IVVGptKCQ3sdYiMIzQKzEXQHCU8h37ycCggEBAJu8aC48Fz3Edlm1
+ldS+2L9vWSaJEBzhoSu0cMBgZVu8SdGzwKDE69XHVI4oS5lI28UFmaaA3JTc07MN
+cAmSGT2oP2NQkPhbXGsrKLfm1K6YAiZ1Ulp7OwxFth8lYreo7Wt92nV46yuqkhDx
+Y3UGhp39xkPhWiRbvgYHxJLsVqFyjumsK2mq3IeNdVZ6VgJXGsTlnAFeqJ7hZxHs
+N5natSRjeosA0PtGJ57agZLvT8Ue0gREef3LzFGoFwmIOcQHZ4kAt2BGOzZDU17H
+6Rb4bKxBEbT1l2St/5zKXi90zDHicOvG7Q8qiyY6HrBc1wLSs+ZtpLxZx/3h3tFE
+IT6fVUECggEBAMSAQm8Ey76OJ+SXUjk1K50442SnHcs/Cmr7urkEQitImUwl71Pk
+87pst/uP6szypOTqmE9yOTIS6iZ6Sn3+QcriIqWrkhZfwW3Tx7S6A7KZUrq15iSH
++thsiw9JXxC9TvOmC8AsBzb2U6hZncsc28JZCxFztSNAduJDb/vhCVLiMxWDFuDr
+kmR1R+yc3XDQRpeQFDz6QudYEj9EPOc6xD/16sZLaqP2+oVFvVSt0tJLsdaQECle
+gMNGAdhE2eX8MCOUHMc+E6cdlozYAEhMFfO2/cqWR79jq3TlVR3dnOFRDScqHMhc
+KnuTvsELjHkUbvGsCSiff7yk+fop7vy4OJsCggEAPemJdItO2rhib8EofrZdY72I
+oifX1jhPZ1BWD2GKgcx+eVyJGbONBbJVexvvskTfZBvCcAegmgp+sngP6MO6yZkr
+cHMfAJeApYZnshsgXksHGMDtSB50/w1JLrc/nqpxdpy/aTazt0Eu1pLWpze1HFZ/
+Xyu4PcmrU+4P1vN7c396slHMktEvly6QqOn4nfBbGDJ17Ow6X1XFvGjAxQPIDTB+
+6loV14AHymwmqwMrGn84O72rzqyw+41GxW5+oXhOZ4MeXF3u89TBLWvXDpPy/YQU
+EiKpodN0YeEn6Ghzplan8rUha+7TP7AYnS5pCszsCHKd03Py0lMLkF+uAfVsDA==
+-----END RSA PRIVATE KEY-----
diff --git a/Tethering/apex/com.android.tethering.pk8 b/Tethering/apex/com.android.tethering.pk8
new file mode 100644
index 0000000..3b94405
--- /dev/null
+++ b/Tethering/apex/com.android.tethering.pk8
Binary files differ
diff --git a/Tethering/apex/com.android.tethering.x509.pem b/Tethering/apex/com.android.tethering.x509.pem
new file mode 100644
index 0000000..a1786e3
--- /dev/null
+++ b/Tethering/apex/com.android.tethering.x509.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGKTCCBBGgAwIBAgIUNiSs5EMqxCZ31gWWCcRJVp9HffAwDQYJKoZIhvcNAQEL
+BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMR4wHAYDVQQDDBVjb20uYW5kcm9pZC50ZXRoZXJpbmcxIjAgBgkqhkiG9w0B
+CQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wIBcNMTkxMjE4MDcwMDQ4WhgPNDc1NzEx
+MTMwNzAwNDhaMIGiMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEW
+MBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UE
+CwwHQW5kcm9pZDEeMBwGA1UEAwwVY29tLmFuZHJvaWQudGV0aGVyaW5nMSIwIAYJ
+KoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEAxvTUA4seblYjZLfTVNwZuJH914QVNFTj+vD94pWmt5Aq
+sH1DVTpBvpXXegc/P5HI2XF/71poSBib1WaQSuXG0fU5K75T18bOGL0qF+fhMtBO
+wUyvulcjO0h4XE/xf0txY54exUjAA4JS9ERGJOgb4GOwSbPyzekfmzIyCZ2Yawwu
++oGwD2ZNzZRaPOoWxjwohBWQ6mySuvF9RRRb300qmxxUGFM9Ki3aqrWlYlHEOwOC
+M+gIXxYFO7S+yUzf6/gMZLOz2YqfcTOup4hAxtExR7niutxJSsRLPBL237exAJoz
+OupoXjtWAlPK4ZwZ/Nl1jdTWauJ+Kv3WqzhHGEb2gn3ZpeO3IdOjJhDgFJ6m1OT/
+kjRbW1LCuKGrKaoqsEDT2X3a7Izfripn65hSNTfR5gNLtgELaI3/vXi8Fmzw1AfH
++qi6ulElZvSwx0qm+S0QiPyGFlxrsdnHoGJl1tzjJW8KdNZRvzRLUQtbphPp+VkL
+5i0bNKum+AwbfdUkLkNLfw9XdbujgBkZTZDQbZGsNjgrvyXcPO2KiJee0hVCZRs0
+rhDi5Pfm7BnN/I2vaTRz/W4mdct9H2RWMuqlSH90JvmKtWcND8ahmOJ3sggrvzfO
+QNs3k4JTRecamMzqIkylhlnEC4FjWc6Bx4wsEpwBMZOkF/tGGMZYf2C09a8tpP0C
+AwEAAaNTMFEwHQYDVR0OBBYEFNP5gIpNWmq0xa411M1GaRPbEijvMB8GA1UdIwQY
+MBaAFNP5gIpNWmq0xa411M1GaRPbEijvMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggIBADJGmU3QP4EGbt6eBhVPeo/efsqrHsuB2fvFzvIobJbfkSob
+cmvjbzIikOlPAgFWj8lT5SDcIWRorFf1u2JylClJ0nSDcqJMHVKmT7wseV/KtX//
+1yUyJFRQVzmjC89dp8OIc00GmItivKLer3NbJdkR3rTUjg7+bNUO27Qp3AFREmiJ
+P+M7ouvcQRvByUWbp/LOrJpMdJLysRBO562RwrtwTjltdvufyYswbBZOKEiUh1Jc
+Ged+3+SJdhwq3Wy+R3Uj7YE7mUMu1QNbANIMrwF8W93EA53eoL2+cKmuaVU6ZURL
+xgSJaY6TrunnSI9XTROLtjsFlJorYWy2tvG7Q5Hw3OkO2Xdz/mm85VTkiusg9DMB
+WWTv607YtsIO0FhKmcV4bp3q/EkRj3t/zLvL9uFJrWDGkuShZq6fQvqbCvaokOPY
++M0ZRIwgwa9UpEE0BMklVWqR6BGyap614gOgcOjYM70WRNl59Qne+g128ZN7g9nz
+61F70i7kUngV0ZUz1/Fu/NCG+6wGF85ZbFmQl60YHPDw1FtjVUuKyBblaDzdJunx
+yQr2t9RUokzFBFK0lGW3+yf0WDQ5fqTMs5h8bz1FCq8/HzWmpdOfqePLe4zsld3b
+1nFuSohaIfbn/HDdTNtTBGQPgz8ZswQ6ejJJqTLz9D/odbqn9LeIhDZXcQTf
+-----END CERTIFICATE-----
diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json
new file mode 100644
index 0000000..11e205d
--- /dev/null
+++ b/Tethering/apex/manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.tethering",
+ "version": 309999900
+}
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
new file mode 100644
index 0000000..33f1c29
--- /dev/null
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -0,0 +1,197 @@
+/*
+ * 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.networkstack.tethering.apishim.api30;
+
+import android.net.INetd;
+import android.net.MacAddress;
+import android.net.TetherStatsParcel;
+import android.net.util.SharedLog;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.TetherStatsValue;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public class BpfCoordinatorShimImpl
+ extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim {
+ private static final String TAG = "api30.BpfCoordinatorShimImpl";
+
+ @NonNull
+ private final SharedLog mLog;
+ @NonNull
+ private final INetd mNetd;
+
+ public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
+ mLog = deps.getSharedLog().forSubComponent(TAG);
+ mNetd = deps.getNetd();
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return true;
+ };
+
+ @Override
+ public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+ try {
+ mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Could not add IPv6 forwarding rule: ", e);
+ return false;
+ }
+
+ return true;
+ };
+
+ @Override
+ public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+ try {
+ mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel());
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Could not remove IPv6 forwarding rule: ", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
+ @NonNull MacAddress outDstMac, int mtu) {
+ return true;
+ }
+
+ @Override
+ public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex,
+ int upstreamIfindex, @NonNull MacAddress inDstMac) {
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
+ final TetherStatsParcel[] tetherStatsList;
+ try {
+ // The reported tether stats are total data usage for all currently-active upstream
+ // interfaces since tethering start. There will only ever be one entry for a given
+ // interface index.
+ tetherStatsList = mNetd.tetherOffloadGetStats();
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Fail to fetch tethering stats from netd: " + e);
+ return null;
+ }
+
+ return toTetherStatsValueSparseArray(tetherStatsList);
+ }
+
+ @Override
+ public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
+ try {
+ mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception when updating quota " + quotaBytes + ": ", e);
+ return false;
+ }
+ return true;
+ }
+
+ @NonNull
+ private SparseArray<TetherStatsValue> toTetherStatsValueSparseArray(
+ @NonNull final TetherStatsParcel[] parcels) {
+ final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+
+ for (TetherStatsParcel p : parcels) {
+ tetherStatsList.put(p.ifIndex, new TetherStatsValue(p.rxPackets, p.rxBytes,
+ 0 /* rxErrors */, p.txPackets, p.txBytes, 0 /* txErrors */));
+ }
+
+ return tetherStatsList;
+ }
+
+ @Override
+ @Nullable
+ public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
+ try {
+ final TetherStatsParcel stats =
+ mNetd.tetherOffloadGetAndClearStats(ifIndex);
+ return new TetherStatsValue(stats.rxPackets, stats.rxBytes, 0 /* rxErrors */,
+ stats.txPackets, stats.txBytes, 0 /* txErrors */);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception when cleanup tether stats for upstream index "
+ + ifIndex + ": ", e);
+ return null;
+ }
+ }
+
+ @Override
+ public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
+ @NonNull Tether4Value value) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean attachProgram(String iface, boolean downstream) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean detachProgram(String iface) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean isAnyIpv4RuleOnUpstream(int ifIndex) {
+ /* no op */
+ return false;
+ }
+
+ @Override
+ public boolean addDevMap(int ifIndex) {
+ /* no op */
+ return false;
+ }
+
+ @Override
+ public boolean removeDevMap(int ifIndex) {
+ /* no op */
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "Netd used";
+ }
+}
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
new file mode 100644
index 0000000..74ddcbc
--- /dev/null
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -0,0 +1,522 @@
+/*
+ * 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.networkstack.tethering.apishim.api31;
+
+import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+
+import android.net.MacAddress;
+import android.net.util.SharedLog;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfMap;
+import com.android.networkstack.tethering.BpfUtils;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.Tether6Value;
+import com.android.networkstack.tethering.TetherDevKey;
+import com.android.networkstack.tethering.TetherDevValue;
+import com.android.networkstack.tethering.TetherDownstream6Key;
+import com.android.networkstack.tethering.TetherLimitKey;
+import com.android.networkstack.tethering.TetherLimitValue;
+import com.android.networkstack.tethering.TetherStatsKey;
+import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream6Key;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public class BpfCoordinatorShimImpl
+ extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim {
+ private static final String TAG = "api31.BpfCoordinatorShimImpl";
+
+ // AF_KEY socket type. See include/linux/socket.h.
+ private static final int AF_KEY = 15;
+ // PFKEYv2 constants. See include/uapi/linux/pfkeyv2.h.
+ private static final int PF_KEY_V2 = 2;
+
+ @NonNull
+ private final SharedLog mLog;
+
+ // BPF map for downstream IPv4 forwarding.
+ @Nullable
+ private final BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+
+ // BPF map for upstream IPv4 forwarding.
+ @Nullable
+ private final BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+
+ // BPF map for downstream IPv6 forwarding.
+ @Nullable
+ private final BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+
+ // BPF map for upstream IPv6 forwarding.
+ @Nullable
+ private final BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+
+ // BPF map of tethering statistics of the upstream interface since tethering startup.
+ @Nullable
+ private final BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+
+ // BPF map of per-interface quota for tethering offload.
+ @Nullable
+ private final BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
+
+ // BPF map of interface index mapping for XDP.
+ @Nullable
+ private final BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
+
+ // Tracking IPv4 rule count while any rule is using the given upstream interfaces. Used for
+ // reducing the BPF map iteration query. The count is increased or decreased when the rule is
+ // added or removed successfully on mBpfDownstream4Map. Counting the rules on downstream4 map
+ // is because tetherOffloadRuleRemove can't get upstream interface index from upstream key,
+ // unless pass upstream value which is not required for deleting map entry. The upstream
+ // interface index is the same in Upstream4Value.oif and Downstream4Key.iif. For now, it is
+ // okay to count on Downstream4Key. See BpfConntrackEventConsumer#accept.
+ // Note that except the constructor, any calls to mBpfDownstream4Map.clear() need to clear
+ // this counter as well.
+ // TODO: Count the rule on upstream if multi-upstream is supported and the
+ // packet needs to be sent and responded on different upstream interfaces.
+ // TODO: Add IPv6 rule count.
+ private final SparseArray<Integer> mRule4CountOnUpstream = new SparseArray<>();
+
+ public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
+ mLog = deps.getSharedLog().forSubComponent(TAG);
+
+ mBpfDownstream4Map = deps.getBpfDownstream4Map();
+ mBpfUpstream4Map = deps.getBpfUpstream4Map();
+ mBpfDownstream6Map = deps.getBpfDownstream6Map();
+ mBpfUpstream6Map = deps.getBpfUpstream6Map();
+ mBpfStatsMap = deps.getBpfStatsMap();
+ mBpfLimitMap = deps.getBpfLimitMap();
+ mBpfDevMap = deps.getBpfDevMap();
+
+ // Clear the stubs of the maps for handling the system service crash if any.
+ // Doesn't throw the exception and clear the stubs as many as possible.
+ try {
+ if (mBpfDownstream4Map != null) mBpfDownstream4Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfDownstream4Map: " + e);
+ }
+ try {
+ if (mBpfUpstream4Map != null) mBpfUpstream4Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfUpstream4Map: " + e);
+ }
+ try {
+ if (mBpfDownstream6Map != null) mBpfDownstream6Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfDownstream6Map: " + e);
+ }
+ try {
+ if (mBpfUpstream6Map != null) mBpfUpstream6Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfUpstream6Map: " + e);
+ }
+ try {
+ if (mBpfStatsMap != null) mBpfStatsMap.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfStatsMap: " + e);
+ }
+ try {
+ if (mBpfLimitMap != null) mBpfLimitMap.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfLimitMap: " + e);
+ }
+ try {
+ if (mBpfDevMap != null) mBpfDevMap.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfDevMap: " + e);
+ }
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return mBpfDownstream4Map != null && mBpfUpstream4Map != null && mBpfDownstream6Map != null
+ && mBpfUpstream6Map != null && mBpfStatsMap != null && mBpfLimitMap != null
+ && mBpfDevMap != null;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+ if (!isInitialized()) return false;
+
+ final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
+ final Tether6Value value = rule.makeTether6Value();
+
+ try {
+ mBpfDownstream6Map.updateEntry(key, value);
+ } catch (ErrnoException e) {
+ mLog.e("Could not update entry: ", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+ if (!isInitialized()) return false;
+
+ try {
+ mBpfDownstream6Map.deleteEntry(rule.makeTetherDownstream6Key());
+ } catch (ErrnoException e) {
+ // Silent if the rule did not exist.
+ if (e.errno != OsConstants.ENOENT) {
+ mLog.e("Could not update entry: ", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
+ @NonNull MacAddress outDstMac, int mtu) {
+ if (!isInitialized()) return false;
+
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac);
+ final Tether6Value value = new Tether6Value(upstreamIfindex, outSrcMac,
+ outDstMac, OsConstants.ETH_P_IPV6, mtu);
+ try {
+ mBpfUpstream6Map.insertEntry(key, value);
+ } catch (ErrnoException | IllegalStateException e) {
+ mLog.e("Could not insert upstream6 entry: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac) {
+ if (!isInitialized()) return false;
+
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac);
+ try {
+ mBpfUpstream6Map.deleteEntry(key);
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete upstream IPv6 entry: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
+ if (!isInitialized()) return null;
+
+ final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+ try {
+ // The reported tether stats are total data usage for all currently-active upstream
+ // interfaces since tethering start.
+ mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value));
+ } catch (ErrnoException e) {
+ mLog.e("Fail to fetch tethering stats from BPF map: ", e);
+ return null;
+ }
+ return tetherStatsList;
+ }
+
+ @Override
+ public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
+ if (!isInitialized()) return false;
+
+ // The common case is an update, where the stats already exist,
+ // hence we read first, even though writing with BPF_NOEXIST
+ // first would make the code simpler.
+ long rxBytes, txBytes;
+ TetherStatsValue statsValue = null;
+
+ try {
+ statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex));
+ } catch (ErrnoException e) {
+ // The BpfMap#getValue doesn't throw an errno ENOENT exception. Catch other error
+ // while trying to get stats entry.
+ mLog.e("Could not get stats entry of interface index " + ifIndex + ": ", e);
+ return false;
+ }
+
+ if (statsValue != null) {
+ // Ok, there was a stats entry.
+ rxBytes = statsValue.rxBytes;
+ txBytes = statsValue.txBytes;
+ } else {
+ // No stats entry - create one with zeroes.
+ try {
+ // This function is the *only* thing that can create entries.
+ // BpfMap#insertEntry use BPF_NOEXIST to create the entry. The entry is created
+ // if and only if it doesn't exist.
+ mBpfStatsMap.insertEntry(new TetherStatsKey(ifIndex), new TetherStatsValue(
+ 0 /* rxPackets */, 0 /* rxBytes */, 0 /* rxErrors */, 0 /* txPackets */,
+ 0 /* txBytes */, 0 /* txErrors */));
+ } catch (ErrnoException | IllegalArgumentException e) {
+ mLog.e("Could not create stats entry: ", e);
+ return false;
+ }
+ rxBytes = 0;
+ txBytes = 0;
+ }
+
+ // rxBytes + txBytes won't overflow even at 5gbps for ~936 years.
+ long newLimit = rxBytes + txBytes + quotaBytes;
+
+ // if adding limit (e.g., if limit is QUOTA_UNLIMITED) caused overflow: clamp to 'infinity'
+ if (newLimit < rxBytes + txBytes) newLimit = QUOTA_UNLIMITED;
+
+ try {
+ mBpfLimitMap.updateEntry(new TetherLimitKey(ifIndex), new TetherLimitValue(newLimit));
+ } catch (ErrnoException e) {
+ mLog.e("Fail to set quota " + quotaBytes + " for interface index " + ifIndex + ": ", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
+ if (!isInitialized()) return null;
+
+ // getAndClearTetherOffloadStats is called after all offload rules have already been
+ // deleted for the given upstream interface. Before starting to do cleanup stuff in this
+ // function, use synchronizeKernelRCU to make sure that all the current running eBPF
+ // programs are finished on all CPUs, especially the unfinished packet processing. After
+ // synchronizeKernelRCU returned, we can safely read or delete on the stats map or the
+ // limit map.
+ final int res = synchronizeKernelRCU();
+ if (res != 0) {
+ // Error log but don't return. Do as much cleanup as possible.
+ mLog.e("synchronize_rcu() failed: " + res);
+ }
+
+ TetherStatsValue statsValue = null;
+ try {
+ statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not get stats entry for interface index " + ifIndex + ": ", e);
+ return null;
+ }
+
+ if (statsValue == null) {
+ mLog.e("Could not get stats entry for interface index " + ifIndex);
+ return null;
+ }
+
+ try {
+ mBpfStatsMap.deleteEntry(new TetherStatsKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete stats entry for interface index " + ifIndex + ": ", e);
+ return null;
+ }
+
+ try {
+ mBpfLimitMap.deleteEntry(new TetherLimitKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete limit for interface index " + ifIndex + ": ", e);
+ return null;
+ }
+
+ return statsValue;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
+ @NonNull Tether4Value value) {
+ if (!isInitialized()) return false;
+
+ try {
+ if (downstream) {
+ mBpfDownstream4Map.insertEntry(key, value);
+
+ // Increase the rule count while a adding rule is using a given upstream interface.
+ final int upstreamIfindex = (int) key.iif;
+ int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */);
+ mRule4CountOnUpstream.put(upstreamIfindex, ++count);
+ } else {
+ mBpfUpstream4Map.insertEntry(key, value);
+ }
+ } catch (ErrnoException e) {
+ mLog.e("Could not insert entry (" + key + ", " + value + "): " + e);
+ return false;
+ } catch (IllegalStateException e) {
+ // Silent if the rule already exists. Note that the errno EEXIST was rethrown as
+ // IllegalStateException. See BpfMap#insertEntry.
+ }
+ return true;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
+ if (!isInitialized()) return false;
+
+ try {
+ if (downstream) {
+ if (!mBpfDownstream4Map.deleteEntry(key)) {
+ mLog.e("Could not delete entry (key: " + key + ")");
+ return false;
+ }
+
+ // Decrease the rule count while a deleting rule is not using a given upstream
+ // interface anymore.
+ final int upstreamIfindex = (int) key.iif;
+ Integer count = mRule4CountOnUpstream.get(upstreamIfindex);
+ if (count == null) {
+ Log.wtf(TAG, "Could not delete count for interface " + upstreamIfindex);
+ return false;
+ }
+
+ if (--count == 0) {
+ // Remove the entry if the count decreases to zero.
+ mRule4CountOnUpstream.remove(upstreamIfindex);
+ } else {
+ mRule4CountOnUpstream.put(upstreamIfindex, count);
+ }
+ } else {
+ mBpfUpstream4Map.deleteEntry(key);
+ }
+ } catch (ErrnoException e) {
+ // Silent if the rule did not exist.
+ if (e.errno != OsConstants.ENOENT) {
+ mLog.e("Could not delete entry: ", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean attachProgram(String iface, boolean downstream) {
+ if (!isInitialized()) return false;
+
+ try {
+ BpfUtils.attachProgram(iface, downstream);
+ } catch (IOException e) {
+ mLog.e("Could not attach program: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean detachProgram(String iface) {
+ if (!isInitialized()) return false;
+
+ try {
+ BpfUtils.detachProgram(iface);
+ } catch (IOException e) {
+ mLog.e("Could not detach program: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isAnyIpv4RuleOnUpstream(int ifIndex) {
+ // No entry means no rule for the given interface because 0 has never been stored.
+ return mRule4CountOnUpstream.get(ifIndex) != null;
+ }
+
+ @Override
+ public boolean addDevMap(int ifIndex) {
+ if (!isInitialized()) return false;
+
+ try {
+ mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not add interface " + ifIndex + ": " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean removeDevMap(int ifIndex) {
+ if (!isInitialized()) return false;
+
+ try {
+ mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete interface " + ifIndex + ": " + e);
+ return false;
+ }
+ return true;
+ }
+
+ private String mapStatus(BpfMap m, String name) {
+ return name + "{" + (m != null ? "OK" : "ERROR") + "}";
+ }
+
+ @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")
+ });
+ }
+
+ /**
+ * Call synchronize_rcu() to block until all existing RCU read-side critical sections have
+ * been completed.
+ * Note that BpfCoordinatorTest have no permissions to create or close pf_key socket. It is
+ * okay for now because the caller #bpfGetAndClearStats doesn't care the result of this
+ * function. The tests don't be broken.
+ * TODO: Wrap this function into Dependencies for mocking in tests.
+ */
+ private int synchronizeKernelRCU() {
+ // This is a temporary hack for network stats map swap on devices running
+ // 4.9 kernels. The kernel code of socket release on pf_key socket will
+ // explicitly call synchronize_rcu() which is exactly what we need.
+ FileDescriptor pfSocket;
+ try {
+ pfSocket = Os.socket(AF_KEY, OsConstants.SOCK_RAW | OsConstants.SOCK_CLOEXEC,
+ PF_KEY_V2);
+ } catch (ErrnoException e) {
+ mLog.e("create PF_KEY socket failed: ", e);
+ return e.errno;
+ }
+
+ // When closing socket, synchronize_rcu() gets called in sock_release().
+ try {
+ Os.close(pfSocket);
+ } catch (ErrnoException e) {
+ mLog.e("failed to close the PF_KEY socket: ", e);
+ return e.errno;
+ }
+
+ return 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
new file mode 100644
index 0000000..8a7a49c
--- /dev/null
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -0,0 +1,180 @@
+/*
+ * 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.networkstack.tethering.apishim.common;
+
+import android.net.MacAddress;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.TetherStatsValue;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public abstract class BpfCoordinatorShim {
+ /**
+ * Get BpfCoordinatorShim object by OS build version.
+ */
+ @NonNull
+ public static BpfCoordinatorShim getBpfCoordinatorShim(@NonNull final Dependencies deps) {
+ if (deps.isAtLeastS()) {
+ return new com.android.networkstack.tethering.apishim.api31.BpfCoordinatorShimImpl(
+ deps);
+ } else {
+ return new com.android.networkstack.tethering.apishim.api30.BpfCoordinatorShimImpl(
+ deps);
+ }
+ }
+
+ /**
+ * Return true if this class has been initialized, otherwise return false.
+ */
+ public abstract boolean isInitialized();
+
+ /**
+ * Adds a tethering offload rule to BPF map, or updates it if it already exists.
+ *
+ * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated
+ * if the input interface and destination prefix match. Otherwise, a new rule will be created.
+ * Note that this can be only called on handler thread.
+ *
+ * @param rule The rule to add or update.
+ */
+ public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule);
+
+ /**
+ * Deletes a tethering offload rule from the BPF map.
+ *
+ * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted
+ * if the destination IP address and the source interface match. It is not an error if there is
+ * no matching rule to delete.
+ *
+ * @param rule The rule to delete.
+ */
+ public abstract boolean tetherOffloadRuleRemove(@NonNull Ipv6ForwardingRule rule);
+
+ /**
+ * Starts IPv6 forwarding between the specified interfaces.
+
+ * @param downstreamIfindex the downstream interface index
+ * @param upstreamIfindex the upstream interface index
+ * @param inDstMac the destination MAC address to use for XDP
+ * @param outSrcMac the source MAC address to use for packets
+ * @param outDstMac the destination MAC address to use for packets
+ * @return true if operation succeeded or was a no-op, false otherwise
+ */
+ public abstract boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
+ @NonNull MacAddress outDstMac, int mtu);
+
+ /**
+ * Stops IPv6 forwarding between the specified interfaces.
+
+ * @param downstreamIfindex the downstream interface index
+ * @param upstreamIfindex the upstream interface index
+ * @param inDstMac the destination MAC address to use for XDP
+ * @return true if operation succeeded or was a no-op, false otherwise
+ */
+ public abstract boolean stopUpstreamIpv6Forwarding(int downstreamIfindex,
+ int upstreamIfindex, @NonNull MacAddress inDstMac);
+
+ /**
+ * Return BPF tethering offload statistics.
+ *
+ * @return an array of TetherStatsValue's, where each entry contains the upstream interface
+ * index and its tethering statistics since tethering was first started.
+ * There will only ever be one entry for a given interface index.
+ */
+ @Nullable
+ public abstract SparseArray<TetherStatsValue> tetherOffloadGetStats();
+
+ /**
+ * Set a per-interface quota for tethering offload.
+ *
+ * @param ifIndex Index of upstream interface
+ * @param quotaBytes The quota defined as the number of bytes, starting from zero and counting
+ * from *now*. A value of QUOTA_UNLIMITED (-1) indicates there is no limit.
+ */
+ @Nullable
+ public abstract boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+
+ /**
+ * Return BPF tethering offload statistics and clear the stats for a given upstream.
+ *
+ * Must only be called once all offload rules have already been deleted for the given upstream
+ * interface. The existing stats will be fetched and returned. The stats and the limit for the
+ * given upstream interface will be deleted as well.
+ *
+ * The stats and limit for a given upstream interface must be initialized (using
+ * tetherOffloadSetInterfaceQuota) before any offload will occur on that interface.
+ *
+ * Note that this can be only called while the BPF maps were initialized.
+ *
+ * @param ifIndex Index of upstream interface.
+ * @return TetherStatsValue, which contains the given upstream interface's tethering statistics
+ * since tethering was first started on that upstream interface.
+ */
+ @Nullable
+ public abstract TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex);
+
+ /**
+ * Adds a tethering IPv4 offload rule to appropriate BPF map.
+ */
+ public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
+ @NonNull Tether4Value value);
+
+ /**
+ * Deletes a tethering IPv4 offload rule from the appropriate BPF map.
+ */
+ public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key);
+
+ /**
+ * Whether there is currently any IPv4 rule on the specified upstream.
+ */
+ public abstract boolean isAnyIpv4RuleOnUpstream(int ifIndex);
+
+ /**
+ * Attach BPF program.
+ *
+ * TODO: consider using InterfaceParams to replace interface name.
+ */
+ public abstract boolean attachProgram(@NonNull String iface, boolean downstream);
+
+ /**
+ * Detach BPF program.
+ *
+ * TODO: consider using InterfaceParams to replace interface name.
+ */
+ public abstract boolean detachProgram(@NonNull String iface);
+
+ /**
+ * Add interface index mapping.
+ */
+ public abstract boolean addDevMap(int ifIndex);
+
+ /**
+ * Remove interface index mapping.
+ */
+ public abstract boolean removeDevMap(int ifIndex);
+}
+
diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp
new file mode 100644
index 0000000..289d75d
--- /dev/null
+++ b/Tethering/bpf_progs/Android.bp
@@ -0,0 +1,68 @@
+//
+// 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.
+//
+
+//
+// struct definitions shared with JNI
+//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_headers {
+ name: "bpf_tethering_headers",
+ vendor_available: false,
+ host_supported: false,
+ export_include_dirs: ["."],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ sdk_version: "30",
+ min_sdk_version: "30",
+ apex_available: ["com.android.tethering"],
+ visibility: [
+ "//packages/modules/Connectivity/Tethering",
+ ],
+}
+
+//
+// bpf kernel programs
+//
+bpf {
+ name: "offload.o",
+ srcs: ["offload.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ include_dirs: [
+ // TODO: get rid of system/netd.
+ "system/netd/bpf_progs", // for bpf_net_helpers.h
+ ],
+}
+
+bpf {
+ name: "test.o",
+ srcs: ["test.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ include_dirs: [
+ // TODO: get rid of system/netd.
+ "system/netd/bpf_progs", // for bpf_net_helpers.h
+ ],
+}
diff --git a/Tethering/bpf_progs/bpf_tethering.h b/Tethering/bpf_progs/bpf_tethering.h
new file mode 100644
index 0000000..5fdf8cd
--- /dev/null
+++ b/Tethering/bpf_progs/bpf_tethering.h
@@ -0,0 +1,217 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+
+// Common definitions for BPF code in the tethering mainline module.
+// These definitions are available to:
+// - The BPF programs in Tethering/bpf_progs/
+// - JNI code that depends on the bpf_tethering_headers library.
+
+#define BPF_TETHER_ERRORS \
+ ERR(INVALID_IP_VERSION) \
+ ERR(LOW_TTL) \
+ ERR(INVALID_TCP_HEADER) \
+ ERR(TCP_CONTROL_PACKET) \
+ ERR(NON_GLOBAL_SRC) \
+ ERR(NON_GLOBAL_DST) \
+ ERR(LOCAL_SRC_DST) \
+ ERR(NO_STATS_ENTRY) \
+ ERR(NO_LIMIT_ENTRY) \
+ ERR(BELOW_IPV4_MTU) \
+ ERR(BELOW_IPV6_MTU) \
+ ERR(LIMIT_REACHED) \
+ ERR(CHANGE_HEAD_FAILED) \
+ ERR(TOO_SHORT) \
+ ERR(HAS_IP_OPTIONS) \
+ ERR(IS_IP_FRAG) \
+ ERR(CHECKSUM) \
+ ERR(NON_TCP_UDP) \
+ ERR(NON_TCP) \
+ ERR(SHORT_L4_HEADER) \
+ ERR(SHORT_TCP_HEADER) \
+ ERR(SHORT_UDP_HEADER) \
+ ERR(UDP_CSUM_ZERO) \
+ ERR(TRUNCATED_IPV4) \
+ ERR(_MAX)
+
+#define ERR(x) BPF_TETHER_ERR_ ##x,
+enum {
+ BPF_TETHER_ERRORS
+};
+#undef ERR
+
+#define ERR(x) #x,
+static const char *bpf_tether_errors[] = {
+ BPF_TETHER_ERRORS
+};
+#undef ERR
+
+// This header file is shared by eBPF kernel programs (C) and netd (C++) and
+// some of the maps are also accessed directly from Java mainline module code.
+//
+// Hence: explicitly pad all relevant structures and assert that their size
+// is the sum of the sizes of their fields.
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+
+#define BPF_PATH_TETHER BPF_PATH "tethering/"
+
+#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
+
+typedef uint32_t TetherStatsKey; // upstream ifindex
+
+typedef struct {
+ uint64_t rxPackets;
+ uint64_t rxBytes;
+ uint64_t rxErrors;
+ uint64_t txPackets;
+ uint64_t txBytes;
+ uint64_t txErrors;
+} TetherStatsValue;
+STRUCT_SIZE(TetherStatsValue, 6 * 8); // 48
+
+#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
+
+typedef uint32_t TetherLimitKey; // upstream ifindex
+typedef uint64_t TetherLimitValue; // in bytes
+
+#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
+#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
+
+#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
+
+#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
+
+// For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
+// because all downstream types that are currently supported (WiFi, USB, Bluetooth and
+// Ethernet) have 6-byte MAC addresses.
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint8_t zero[2]; // zero pad for 8 byte alignment
+ struct in6_addr neigh6; // The destination IPv6 address
+} TetherDownstream6Key;
+STRUCT_SIZE(TetherDownstream6Key, 4 + 6 + 2 + 16); // 28
+
+typedef struct {
+ uint32_t oif; // The output interface to redirect to
+ struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress)
+ uint16_t pmtu; // The maximum L3 output path/route mtu
+} Tether6Value;
+STRUCT_SIZE(Tether6Value, 4 + 14 + 2); // 20
+
+#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint16_t l4Proto; // IPPROTO_TCP/UDP/...
+ struct in6_addr src6; // source &
+ struct in6_addr dst6; // destination IPv6 addresses
+ __be16 srcPort; // source &
+ __be16 dstPort; // destination tcp/udp/... ports
+} TetherDownstream64Key;
+STRUCT_SIZE(TetherDownstream64Key, 4 + 6 + 2 + 16 + 16 + 2 + 2); // 48
+
+typedef struct {
+ uint32_t oif; // The output interface to redirect to
+ struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress)
+ uint16_t pmtu; // The maximum L3 output path/route mtu
+ struct in_addr src4; // source &
+ struct in_addr dst4; // destination IPv4 addresses
+ __be16 srcPort; // source &
+ __be16 outPort; // destination tcp/udp/... ports
+ uint64_t lastUsed; // Kernel updates on each use with bpf_ktime_get_boot_ns()
+} TetherDownstream64Value;
+STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8); // 40
+
+#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
+#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
+
+#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint8_t zero[2]; // zero pad for 8 byte alignment
+ // TODO: extend this to include src ip /64 subnet
+} TetherUpstream6Key;
+STRUCT_SIZE(TetherUpstream6Key, 12);
+
+#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
+#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
+
+#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
+
+#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
+
+
+#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
+#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
+
+#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint16_t l4Proto; // IPPROTO_TCP/UDP/...
+ struct in_addr src4; // source &
+ struct in_addr dst4; // destination IPv4 addresses
+ __be16 srcPort; // source &
+ __be16 dstPort; // destination TCP/UDP/... ports
+} Tether4Key;
+STRUCT_SIZE(Tether4Key, 4 + 6 + 2 + 4 + 4 + 2 + 2); // 24
+
+typedef struct {
+ uint32_t oif; // The output interface to redirect to
+ struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress)
+ uint16_t pmtu; // Maximum L3 output path/route mtu
+ struct in6_addr src46; // source & (always IPv4 mapped for downstream)
+ struct in6_addr dst46; // destination IP addresses (may be IPv4 mapped or IPv6 for upstream)
+ __be16 srcPort; // source &
+ __be16 dstPort; // destination tcp/udp/... ports
+ uint64_t last_used; // Kernel updates on each use with bpf_ktime_get_boot_ns()
+} Tether4Value;
+STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8); // 64
+
+#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
+#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
+
+#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
+#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
+
+#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
+
+#undef STRUCT_SIZE
diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c
new file mode 100644
index 0000000..6ff370c
--- /dev/null
+++ b/Tethering/bpf_progs/offload.c
@@ -0,0 +1,838 @@
+/*
+ * 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.
+ */
+
+#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>
+
+#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
+#include "bpf_tethering.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 -----
+
+DEFINE_BPF_MAP_GRW(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; \
+ uint32_t *count = bpf_tether_error_map_lookup_elem(&code); \
+ if (count) __sync_fetch_and_add(count, 1); \
+ return ret; \
+} while(0)
+
+#define TC_DROP(counter) COUNT_AND_RETURN(counter, TC_ACT_SHOT)
+#define TC_PUNT(counter) COUNT_AND_RETURN(counter, TC_ACT_OK)
+
+#define XDP_DROP(counter) COUNT_AND_RETURN(counter, XDP_DROP)
+#define XDP_PUNT(counter) COUNT_AND_RETURN(counter, XDP_PASS)
+
+// ----- Tethering Data Stats and Limits -----
+
+// Tethering stats, indexed by upstream interface.
+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, AID_NETWORK_STACK)
+
+// ----- IPv6 Support -----
+
+DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
+ AID_NETWORK_STACK)
+
+DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
+ 1024, AID_NETWORK_STACK)
+
+DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
+ AID_NETWORK_STACK)
+
+static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
+ const bool downstream) {
+ // Must be meta-ethernet IPv6 frame
+ if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_OK;
+
+ // Require ethernet dst mac address to be our unicast address.
+ if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_OK;
+
+ const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+ // 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_readable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ struct ipv6hdr* ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
+ // Must have (ethernet and) ipv6 header
+ if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_OK;
+
+ // Ethertype - if present - must be IPv6
+ if (is_ethernet && (eth->h_proto != htons(ETH_P_IPV6))) return TC_ACT_OK;
+
+ // IP version must be 6
+ if (ip6->version != 6) TC_PUNT(INVALID_IP_VERSION);
+
+ // Cannot decrement during forward if already zero or would be zero,
+ // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+ if (ip6->hop_limit <= 1) TC_PUNT(LOW_TTL);
+
+ // If hardware offload is running and programming flows based on conntrack entries,
+ // try not to interfere with it.
+ if (ip6->nexthdr == IPPROTO_TCP) {
+ struct tcphdr* tcph = (void*)(ip6 + 1);
+
+ // Make sure we can get at the tcp header
+ if (data + l2_header_size + sizeof(*ip6) + sizeof(*tcph) > data_end)
+ TC_PUNT(INVALID_TCP_HEADER);
+
+ // Do not offload TCP packets with any one of the SYN/FIN/RST flags
+ if (tcph->syn || tcph->fin || tcph->rst) TC_PUNT(TCP_CONTROL_PACKET);
+ }
+
+ // Protect against forwarding packets sourced from ::1 or fe80::/64 or other weirdness.
+ __be32 src32 = ip6->saddr.s6_addr32[0];
+ if (src32 != htonl(0x0064ff9b) && // 64:ff9b:/32 incl. XLAT464 WKP
+ (src32 & htonl(0xe0000000)) != htonl(0x20000000)) // 2000::/3 Global Unicast
+ TC_PUNT(NON_GLOBAL_SRC);
+
+ // Protect against forwarding packets destined to ::1 or fe80::/64 or other weirdness.
+ __be32 dst32 = ip6->daddr.s6_addr32[0];
+ if (dst32 != htonl(0x0064ff9b) && // 64:ff9b:/32 incl. XLAT464 WKP
+ (dst32 & htonl(0xe0000000)) != htonl(0x20000000)) // 2000::/3 Global Unicast
+ TC_PUNT(NON_GLOBAL_DST);
+
+ // In the upstream direction do not forward traffic within the same /64 subnet.
+ if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+ TC_PUNT(LOCAL_SRC_DST);
+
+ TetherDownstream6Key kd = {
+ .iif = skb->ifindex,
+ .neigh6 = ip6->daddr,
+ };
+
+ TetherUpstream6Key ku = {
+ .iif = skb->ifindex,
+ };
+ if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+
+ Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
+ : bpf_tether_upstream6_map_lookup_elem(&ku);
+
+ // If we don't find any offload information then simply let the core stack handle it...
+ if (!v) return TC_ACT_OK;
+
+ uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+
+ TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
+
+ // If we don't have anywhere to put stats, then abort...
+ if (!stat_v) TC_PUNT(NO_STATS_ENTRY);
+
+ uint64_t* limit_v = bpf_tether_limit_map_lookup_elem(&stat_and_limit_k);
+
+ // If we don't have a limit, then abort...
+ if (!limit_v) TC_PUNT(NO_LIMIT_ENTRY);
+
+ // Required IPv6 minimum mtu is 1280, below that not clear what we should do, abort...
+ if (v->pmtu < IPV6_MIN_MTU) TC_PUNT(BELOW_IPV6_MTU);
+
+ // Approximate handling of TCP/IPv6 overhead for incoming LRO/GRO packets: default
+ // outbound path mtu of 1500 is not necessarily correct, but worst case we simply
+ // undercount, which is still better then not accounting for this overhead at all.
+ // Note: this really shouldn't be device/path mtu at all, but rather should be
+ // derived from this particular connection's mss (ie. from gro segment size).
+ // This would require a much newer kernel with newer ebpf accessors.
+ // (This is also blindly assuming 12 bytes of tcp timestamp option in tcp header)
+ uint64_t packets = 1;
+ uint64_t bytes = skb->len;
+ if (bytes > v->pmtu) {
+ const int tcp_overhead = sizeof(struct ipv6hdr) + sizeof(struct tcphdr) + 12;
+ const int mss = v->pmtu - tcp_overhead;
+ const uint64_t payload = bytes - tcp_overhead;
+ packets = (payload + mss - 1) / mss;
+ bytes = tcp_overhead * packets + payload;
+ }
+
+ // Are we past the limit? If so, then abort...
+ // Note: will not overflow since u64 is 936 years even at 5Gbps.
+ // Do not drop here. Offload is just that, whenever we fail to handle
+ // a packet we let the core stack deal with things.
+ // (The core stack needs to handle limits correctly anyway,
+ // since we don't offload all traffic in both directions)
+ if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) TC_PUNT(LIMIT_REACHED);
+
+ if (!is_ethernet) {
+ // Try to inject an ethernet header, and simply return if we fail.
+ // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
+ // because this is easier and the kernel will strip extraneous ethernet header.
+ if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_PUNT(CHANGE_HEAD_FAILED);
+ }
+
+ // bpf_skb_change_head() invalidates all pointers - reload them
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+ eth = data;
+ ip6 = (void*)(eth + 1);
+
+ // I do not believe this can ever happen, but keep the verifier happy...
+ if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_DROP(TOO_SHORT);
+ }
+ };
+
+ // At this point we always have an ethernet header - which will get stripped by the
+ // kernel during transmit through a rawip interface. ie. 'eth' pointer is valid.
+ // Additionally note that 'is_ethernet' and 'l2_header_size' are no longer correct.
+
+ // CHECKSUM_COMPLETE is a 16-bit one's complement sum,
+ // thus corrections for it need to be done in 16-byte chunks at even offsets.
+ // IPv6 nexthdr is at offset 6, while hop limit is at offset 7
+ uint8_t old_hl = ip6->hop_limit;
+ --ip6->hop_limit;
+ uint8_t new_hl = ip6->hop_limit;
+
+ // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error
+ // (-ENOTSUPP) if it isn't.
+ bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
+
+ __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, bytes);
+
+ // Overwrite any mac header with the new one
+ // For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
+ *eth = v->macHeader;
+
+ // Redirect to forwarded interface.
+ //
+ // Note that bpf_redirect() cannot fail unless you pass invalid flags.
+ // The redirect actually happens after the ebpf program has already terminated,
+ // and can fail for example for mtu reasons at that point in time, but there's nothing
+ // we can do about it here.
+ return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
+}
+
+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, /* is_ethernet */ true, /* downstream */ true);
+}
+
+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, /* is_ethernet */ true, /* downstream */ false);
+}
+
+// Note: section names must be unique to prevent programs from appending to each other,
+// so instead the bpf loader will strip everything past the final $ symbol when actually
+// pinning the program into the filesystem.
+//
+// bpf_skb_change_head() is only present on 4.14+ and 2 trivial kernel patches are needed:
+// ANDROID: net: bpf: Allow TC programs to call BPF_FUNC_skb_change_head
+// ANDROID: net: bpf: permit redirect from ingress L3 to egress L2 devices at near max mtu
+// (the first of those has already been upstreamed)
+//
+// 5.4 kernel support was only added to Android Common Kernel in R,
+// and thus a 5.4 kernel always supports this.
+//
+// Hence, these mandatory (must load successfully) implementations for 5.4+ kernels:
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_rawip_5_4, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true);
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_rawip_5_4, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false);
+}
+
+// and these identical optional (may fail to load) implementations for [4.14..5.4) patched kernels:
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false);
+}
+
+// and define no-op stubs for [4.9,4.14) and unpatched [4.14,5.4) kernels.
+// (if the above real 4.14+ program loaded successfully, then bpfloader will have already pinned
+// it at the same location this one would be pinned at and will thus skip loading this stub)
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+// ----- IPv4 Support -----
+
+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, AID_NETWORK_STACK)
+
+static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
+ const bool downstream, const bool updatetime) {
+ // Require ethernet dst mac address to be our unicast address.
+ if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_OK;
+
+ // Must be meta-ethernet IPv4 frame
+ if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_OK;
+
+ const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+ // 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_readable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
+
+ // Must have (ethernet and) ipv4 header
+ if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_OK;
+
+ // Ethertype - if present - must be IPv4
+ if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_OK;
+
+ // IP version must be 4
+ if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
+
+ // Calculate the IPv4 one's complement checksum of the IPv4 header.
+ __wsum sum4 = 0;
+ for (int 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
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16
+ // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+ if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
+
+ // Minimum IPv4 total length is the size of the header
+ if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
+
+ // We are incapable of dealing with IPv4 fragments
+ if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
+
+ // Cannot decrement during forward if already zero or would be zero,
+ // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+ if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
+
+ // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
+ // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
+ // in such a situation we can only support TCP. This also has the added nice benefit of
+ // using a separate error counter, and thus making it obvious which version of the program
+ // is loaded.
+ if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+
+ // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
+ // but no need to check this if !updatetime due to check immediately above.
+ if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+ TC_PUNT(NON_TCP_UDP);
+
+ // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
+ // out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
+ const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+
+ // This is a bit of a hack to make things easier on the bpf verifier.
+ // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
+ // what offsets into the packet are valid and can spuriously reject the program, this is
+ // because it fails to realize that is_tcp && !is_tcp is impossible)
+ //
+ // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
+ // always be in the first 4 bytes of the L4 header. Additionally for UDP we'll need access
+ // to the checksum field which is in bytes 7 and 8. While for TCP we'll need to read the
+ // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
+ // As such we *always* need access to at least 8 bytes.
+ if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
+
+ struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
+ struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
+
+ if (is_tcp) {
+ // Make sure we can get at the tcp header
+ if (data + l2_header_size + sizeof(*ip) + sizeof(*tcph) > data_end)
+ TC_PUNT(SHORT_TCP_HEADER);
+
+ // If hardware offload is running and programming flows based on conntrack entries, try not
+ // to interfere with it, so do not offload TCP packets with any one of the SYN/FIN/RST flags
+ if (tcph->syn || tcph->fin || tcph->rst) TC_PUNT(TCP_CONTROL_PACKET);
+ } else { // UDP
+ // Make sure we can get at the udp header
+ if (data + l2_header_size + sizeof(*ip) + sizeof(*udph) > data_end)
+ TC_PUNT(SHORT_UDP_HEADER);
+
+ // Skip handling of CHECKSUM_COMPLETE packets with udp checksum zero due to need for
+ // additional updating of skb->csum (this could be fixed up manually with more effort).
+ //
+ // Note that the in-kernel implementation of 'int64_t bpf_csum_update(skb, u32 csum)' is:
+ // if (skb->ip_summed == CHECKSUM_COMPLETE)
+ // return (skb->csum = csum_add(skb->csum, csum));
+ // else
+ // return -ENOTSUPP;
+ //
+ // So this will punt any CHECKSUM_COMPLETE packet with a zero UDP checksum,
+ // and leave all other packets unaffected (since it just at most adds zero to skb->csum).
+ //
+ // In practice this should almost never trigger because most nics do not generate
+ // CHECKSUM_COMPLETE packets on receive - especially so for nics/drivers on a phone.
+ //
+ // Additionally since we're forwarding, in most cases the value of the skb->csum field
+ // shouldn't matter (it's not used by physical nic egress).
+ //
+ // It only matters if we're ingressing through a CHECKSUM_COMPLETE capable nic
+ // and egressing through a virtual interface looping back to the kernel itself
+ // (ie. something like veth) where the CHECKSUM_COMPLETE/skb->csum can get reused
+ // on ingress.
+ //
+ // If we were in the kernel we'd simply probably call
+ // void skb_checksum_complete_unset(struct sk_buff *skb) {
+ // if (skb->ip_summed == CHECKSUM_COMPLETE) skb->ip_summed = CHECKSUM_NONE;
+ // }
+ // here instead. Perhaps there should be a bpf helper for that?
+ if (!udph->check && (bpf_csum_update(skb, 0) >= 0)) TC_PUNT(UDP_CSUM_ZERO);
+ }
+
+ Tether4Key k = {
+ .iif = skb->ifindex,
+ .l4Proto = ip->protocol,
+ .src4.s_addr = ip->saddr,
+ .dst4.s_addr = ip->daddr,
+ .srcPort = is_tcp ? tcph->source : udph->source,
+ .dstPort = is_tcp ? tcph->dest : udph->dest,
+ };
+ if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
+
+ Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
+ : bpf_tether_upstream4_map_lookup_elem(&k);
+
+ // If we don't find any offload information then simply let the core stack handle it...
+ if (!v) return TC_ACT_OK;
+
+ uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+
+ TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
+
+ // If we don't have anywhere to put stats, then abort...
+ if (!stat_v) TC_PUNT(NO_STATS_ENTRY);
+
+ uint64_t* limit_v = bpf_tether_limit_map_lookup_elem(&stat_and_limit_k);
+
+ // If we don't have a limit, then abort...
+ if (!limit_v) TC_PUNT(NO_LIMIT_ENTRY);
+
+ // Required IPv4 minimum mtu is 68, below that not clear what we should do, abort...
+ if (v->pmtu < 68) TC_PUNT(BELOW_IPV4_MTU);
+
+ // Approximate handling of TCP/IPv4 overhead for incoming LRO/GRO packets: default
+ // outbound path mtu of 1500 is not necessarily correct, but worst case we simply
+ // undercount, which is still better then not accounting for this overhead at all.
+ // Note: this really shouldn't be device/path mtu at all, but rather should be
+ // derived from this particular connection's mss (ie. from gro segment size).
+ // This would require a much newer kernel with newer ebpf accessors.
+ // (This is also blindly assuming 12 bytes of tcp timestamp option in tcp header)
+ uint64_t packets = 1;
+ uint64_t bytes = skb->len;
+ if (bytes > v->pmtu) {
+ const int tcp_overhead = sizeof(struct iphdr) + sizeof(struct tcphdr) + 12;
+ const int mss = v->pmtu - tcp_overhead;
+ const uint64_t payload = bytes - tcp_overhead;
+ packets = (payload + mss - 1) / mss;
+ bytes = tcp_overhead * packets + payload;
+ }
+
+ // Are we past the limit? If so, then abort...
+ // Note: will not overflow since u64 is 936 years even at 5Gbps.
+ // Do not drop here. Offload is just that, whenever we fail to handle
+ // a packet we let the core stack deal with things.
+ // (The core stack needs to handle limits correctly anyway,
+ // since we don't offload all traffic in both directions)
+ if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) TC_PUNT(LIMIT_REACHED);
+
+ if (!is_ethernet) {
+ // Try to inject an ethernet header, and simply return if we fail.
+ // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
+ // because this is easier and the kernel will strip extraneous ethernet header.
+ if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_PUNT(CHANGE_HEAD_FAILED);
+ }
+
+ // bpf_skb_change_head() invalidates all pointers - reload them
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+ eth = data;
+ ip = (void*)(eth + 1);
+ tcph = is_tcp ? (void*)(ip + 1) : NULL;
+ udph = is_tcp ? NULL : (void*)(ip + 1);
+
+ // I do not believe this can ever happen, but keep the verifier happy...
+ if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_DROP(TOO_SHORT);
+ }
+ };
+
+ // At this point we always have an ethernet header - which will get stripped by the
+ // kernel during transmit through a rawip interface. ie. 'eth' pointer is valid.
+ // Additionally note that 'is_ethernet' and 'l2_header_size' are no longer correct.
+
+ // Overwrite any mac header with the new one
+ // For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
+ *eth = v->macHeader;
+
+ const int l4_offs_csum = is_tcp ? ETH_IP4_TCP_OFFSET(check) : ETH_IP4_UDP_OFFSET(check);
+ const int sz4 = sizeof(__be32);
+ // UDP 0 is special and stored as FFFF (this flag also causes a csum of 0 to be unmodified)
+ const int l4_flags = is_tcp ? 0 : BPF_F_MARK_MANGLED_0;
+ const __be32 old_daddr = k.dst4.s_addr;
+ const __be32 old_saddr = k.src4.s_addr;
+ const __be32 new_daddr = v->dst46.s6_addr32[3];
+ const __be32 new_saddr = v->src46.s6_addr32[3];
+
+ bpf_l4_csum_replace(skb, l4_offs_csum, old_daddr, new_daddr, sz4 | BPF_F_PSEUDO_HDR | l4_flags);
+ bpf_l3_csum_replace(skb, ETH_IP4_OFFSET(check), old_daddr, new_daddr, sz4);
+ bpf_skb_store_bytes(skb, ETH_IP4_OFFSET(daddr), &new_daddr, sz4, 0);
+
+ bpf_l4_csum_replace(skb, l4_offs_csum, old_saddr, new_saddr, sz4 | BPF_F_PSEUDO_HDR | l4_flags);
+ bpf_l3_csum_replace(skb, ETH_IP4_OFFSET(check), old_saddr, new_saddr, sz4);
+ bpf_skb_store_bytes(skb, ETH_IP4_OFFSET(saddr), &new_saddr, sz4, 0);
+
+ const int sz2 = sizeof(__be16);
+ // The offsets for TCP and UDP ports: source (u16 @ L4 offset 0) & dest (u16 @ L4 offset 2) are
+ // actually the same, so the compiler should just optimize them both down to a constant.
+ bpf_l4_csum_replace(skb, l4_offs_csum, k.srcPort, v->srcPort, sz2 | l4_flags);
+ bpf_skb_store_bytes(skb, is_tcp ? ETH_IP4_TCP_OFFSET(source) : ETH_IP4_UDP_OFFSET(source),
+ &v->srcPort, sz2, 0);
+
+ bpf_l4_csum_replace(skb, l4_offs_csum, k.dstPort, v->dstPort, sz2 | l4_flags);
+ bpf_skb_store_bytes(skb, is_tcp ? ETH_IP4_TCP_OFFSET(dest) : ETH_IP4_UDP_OFFSET(dest),
+ &v->dstPort, sz2, 0);
+
+ // TEMP HACK: lack of TTL decrement
+
+ // This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
+ // and backported to all Android Common Kernel 4.14+ trees.
+ if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+
+ __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, bytes);
+
+ // Redirect to forwarded interface.
+ //
+ // Note that bpf_redirect() cannot fail unless you pass invalid flags.
+ // The redirect actually happens after the ebpf program has already terminated,
+ // and can fail for example for mtu reasons at that point in time, but there's nothing
+ // we can do about it here.
+ return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
+}
+
+// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
+
+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, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
+}
+
+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, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
+}
+
+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, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
+}
+
+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, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
+}
+
+// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
+// (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",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_ether_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_ether_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
+}
+
+// Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
+// These will be loaded only if the above optional ones failed (loading of *these* must succeed
+// for 5.4+, since that is always an R patched kernel).
+//
+// [Note: as a result TCP connections will not have their conntrack timeout refreshed, however,
+// since /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established defaults to 432000 (seconds),
+// this in practice means they'll break only after 5 days. This seems an acceptable trade-off.
+//
+// Additionally kernel/tests change "net-test: add bpf_ktime_get_ns / bpf_ktime_get_boot_ns tests"
+// which enforces and documents the required kernel cherrypicks will make it pretty unlikely that
+// many devices upgrading to S will end up relying on these fallback programs.
+
+// 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", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
+}
+
+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, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
+}
+
+// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
+// [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",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
+}
+
+// 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", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false);
+}
+
+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, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ false);
+}
+
+// Placeholder (no-op) implementations for older Q kernels
+
+// 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", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+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, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+// ETHER: 4.9-P/Q kernel
+
+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, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+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, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+// ----- XDP Support -----
+
+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 bool is_ethernet,
+ const bool downstream) {
+ return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
+ const bool downstream) {
+ return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+ const void* data = (void*)(long)ctx->data;
+ const void* data_end = (void*)(long)ctx->data_end;
+ const struct ethhdr* eth = data;
+
+ // Make sure we actually have an ethernet header
+ if ((void*)(eth + 1) > data_end) return XDP_PASS;
+
+ if (eth->h_proto == htons(ETH_P_IPV6))
+ return do_xdp_forward6(ctx, /* is_ethernet */ true, downstream);
+ if (eth->h_proto == htons(ETH_P_IP))
+ return do_xdp_forward4(ctx, /* is_ethernet */ true, downstream);
+
+ // Anything else we don't know how to handle...
+ return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+ const void* data = (void*)(long)ctx->data;
+ const void* data_end = (void*)(long)ctx->data_end;
+
+ // The top nibble of both IPv4 and IPv6 headers is the IP version.
+ if (data_end - data < 1) return XDP_PASS;
+ const uint8_t v = (*(uint8_t*)data) >> 4;
+
+ if (v == 6) return do_xdp_forward6(ctx, /* is_ethernet */ false, downstream);
+ if (v == 4) return do_xdp_forward4(ctx, /* is_ethernet */ false, downstream);
+
+ // Anything else we don't know how to handle...
+ return XDP_PASS;
+}
+
+#define DEFINE_XDP_PROG(str, func) \
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+
+DEFINE_XDP_PROG("xdp/tether_downstream_ether",
+ xdp_tether_downstream_ether) {
+ return do_xdp_forward_ether(ctx, /* downstream */ true);
+}
+
+DEFINE_XDP_PROG("xdp/tether_downstream_rawip",
+ xdp_tether_downstream_rawip) {
+ return do_xdp_forward_rawip(ctx, /* downstream */ true);
+}
+
+DEFINE_XDP_PROG("xdp/tether_upstream_ether",
+ xdp_tether_upstream_ether) {
+ return do_xdp_forward_ether(ctx, /* downstream */ false);
+}
+
+DEFINE_XDP_PROG("xdp/tether_upstream_rawip",
+ xdp_tether_upstream_rawip) {
+ return do_xdp_forward_rawip(ctx, /* downstream */ false);
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("tethering");
diff --git a/Tethering/bpf_progs/test.c b/Tethering/bpf_progs/test.c
new file mode 100644
index 0000000..3f0df2e
--- /dev/null
+++ b/Tethering/bpf_progs/test.c
@@ -0,0 +1,47 @@
+/*
+ * 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/if_ether.h>
+#include <linux/in.h>
+#include <linux/ip.h>
+
+#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
+#include "bpf_tethering.h"
+
+// Used only by TetheringPrivilegedTests, not by production code.
+DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
+ AID_NETWORK_STACK)
+
+DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
+ xdp_test, KVER(5, 9, 0))
+(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;
+}
+
+LICENSE("Apache 2.0");
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
new file mode 100644
index 0000000..fce4360
--- /dev/null
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -0,0 +1,63 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_sdk_library {
+ name: "framework-tethering",
+ defaults: ["framework-module-defaults"],
+ impl_library_visibility: [
+ "//packages/modules/Connectivity/Tethering:__subpackages__",
+ ],
+
+ srcs: [":framework-tethering-srcs"],
+ libs: ["framework-connectivity.stubs.module_lib"],
+ stub_only_libs: ["framework-connectivity.stubs.module_lib"],
+ aidl: {
+ include_dirs: [
+ "packages/modules/Connectivity/framework/aidl-export",
+ ],
+ },
+
+ jarjar_rules: "jarjar-rules.txt",
+ installable: true,
+
+ hostdex: true, // for hiddenapi check
+ apex_available: ["com.android.tethering"],
+ permitted_packages: ["android.net"],
+ min_sdk_version: "30",
+}
+
+filegroup {
+ name: "framework-tethering-srcs",
+ srcs: [
+ "src/android/net/TetheredClient.aidl",
+ "src/android/net/TetheredClient.java",
+ "src/android/net/TetheringManager.java",
+ "src/android/net/TetheringConstants.java",
+ "src/android/net/IIntResultListener.aidl",
+ "src/android/net/ITetheringEventCallback.aidl",
+ "src/android/net/ITetheringConnector.aidl",
+ "src/android/net/TetheringCallbackStartedParcel.aidl",
+ "src/android/net/TetheringConfigurationParcel.aidl",
+ "src/android/net/TetheringRequestParcel.aidl",
+ "src/android/net/TetherStatesParcel.aidl",
+ "src/android/net/TetheringInterface.aidl",
+ "src/android/net/TetheringInterface.java",
+ ],
+ path: "src"
+}
diff --git a/Tethering/common/TetheringLib/api/current.txt b/Tethering/common/TetheringLib/api/current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
new file mode 100644
index 0000000..0566040
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -0,0 +1,41 @@
+// Signature format: 2.0
+package android.net {
+
+ public final class TetheringConstants {
+ field public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType";
+ field public static final String EXTRA_PROVISION_CALLBACK = "extraProvisionCallback";
+ field public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType";
+ field public static final String EXTRA_RUN_PROVISION = "extraRunProvision";
+ field public static final String EXTRA_SET_ALARM = "extraSetAlarm";
+ }
+
+ public class TetheringManager {
+ ctor public TetheringManager(@NonNull android.content.Context, @NonNull java.util.function.Supplier<android.os.IBinder>);
+ method public int getLastTetherError(@NonNull String);
+ method @NonNull public String[] getTetherableBluetoothRegexs();
+ method @NonNull public String[] getTetherableIfaces();
+ method @NonNull public String[] getTetherableUsbRegexs();
+ method @NonNull public String[] getTetherableWifiRegexs();
+ method @NonNull public String[] getTetheredIfaces();
+ method @NonNull public String[] getTetheringErroredIfaces();
+ method public boolean isTetheringSupported();
+ method public boolean isTetheringSupported(@NonNull String);
+ method public void requestLatestTetheringEntitlementResult(int, @NonNull android.os.ResultReceiver, boolean);
+ method @Deprecated public int setUsbTethering(boolean);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+ method @Deprecated public int tether(@NonNull String);
+ method @Deprecated public int untether(@NonNull String);
+ }
+
+ public static interface TetheringManager.TetheringEventCallback {
+ method @Deprecated public default void onTetherableInterfaceRegexpsChanged(@NonNull android.net.TetheringManager.TetheringInterfaceRegexps);
+ }
+
+ @Deprecated public static class TetheringManager.TetheringInterfaceRegexps {
+ method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableBluetoothRegexs();
+ method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableUsbRegexs();
+ method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableWifiRegexs();
+ }
+
+}
+
diff --git a/Tethering/common/TetheringLib/api/module-lib-removed.txt b/Tethering/common/TetheringLib/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/api/removed.txt b/Tethering/common/TetheringLib/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
new file mode 100644
index 0000000..844ff64
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -0,0 +1,117 @@
+// Signature format: 2.0
+package android.net {
+
+ public final class TetheredClient implements android.os.Parcelable {
+ ctor public TetheredClient(@NonNull android.net.MacAddress, @NonNull java.util.Collection<android.net.TetheredClient.AddressInfo>, int);
+ method public int describeContents();
+ method @NonNull public java.util.List<android.net.TetheredClient.AddressInfo> getAddresses();
+ method @NonNull public android.net.MacAddress getMacAddress();
+ method public int getTetheringType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient> CREATOR;
+ }
+
+ public static final class TetheredClient.AddressInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.net.LinkAddress getAddress();
+ method @Nullable public String getHostname();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient.AddressInfo> CREATOR;
+ }
+
+ public final class TetheringInterface implements android.os.Parcelable {
+ ctor public TetheringInterface(int, @NonNull String);
+ method public int describeContents();
+ method @NonNull public String getInterface();
+ method public int getType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
+ }
+
+ public class TetheringManager {
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+ field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
+ field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
+ field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
+ field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
+ field public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
+ field public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
+ field public static final String EXTRA_ERRORED_TETHER = "erroredArray";
+ field public static final int TETHERING_BLUETOOTH = 2; // 0x2
+ field public static final int TETHERING_ETHERNET = 5; // 0x5
+ 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 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
+ field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
+ field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
+ field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
+ field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
+ field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
+ field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
+ field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
+ field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
+ field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
+ field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
+ field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
+ field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
+ field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
+ field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
+ field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
+ field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
+ field public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; // 0x2
+ field public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; // 0x1
+ field public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; // 0x0
+ }
+
+ public static interface TetheringManager.OnTetheringEntitlementResultListener {
+ method public void onTetheringEntitlementResult(int);
+ }
+
+ public static interface TetheringManager.StartTetheringCallback {
+ method public default void onTetheringFailed(int);
+ method public default void onTetheringStarted();
+ }
+
+ public static interface TetheringManager.TetheringEventCallback {
+ method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
+ method public default void onError(@NonNull String, int);
+ method public default void onError(@NonNull android.net.TetheringInterface, int);
+ method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.List<java.lang.String>);
+ method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
+ method public default void onOffloadStatusChanged(int);
+ method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
+ method public default void onTetherableInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
+ method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
+ method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
+ method public default void onTetheringSupported(boolean);
+ method public default void onUpstreamChanged(@Nullable android.net.Network);
+ }
+
+ public static class TetheringManager.TetheringRequest {
+ method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
+ method public int getConnectivityScope();
+ method @Nullable public android.net.LinkAddress getLocalIpv4Address();
+ method public boolean getShouldShowEntitlementUi();
+ method public int getTetheringType();
+ method public boolean isExemptFromEntitlementCheck();
+ }
+
+ public static class TetheringManager.TetheringRequest.Builder {
+ ctor public TetheringManager.TetheringRequest.Builder(int);
+ method @NonNull public android.net.TetheringManager.TetheringRequest build();
+ 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 @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/api/system-removed.txt b/Tethering/common/TetheringLib/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/Tethering/common/TetheringLib/jarjar-rules.txt b/Tethering/common/TetheringLib/jarjar-rules.txt
new file mode 100644
index 0000000..e459fad
--- /dev/null
+++ b/Tethering/common/TetheringLib/jarjar-rules.txt
@@ -0,0 +1 @@
+# jarjar rules for the bootclasspath tethering framework library here
\ No newline at end of file
diff --git a/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl b/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl
new file mode 100644
index 0000000..c3d66ee
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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 android.net;
+
+/**
+ * Listener interface allowing objects to listen to various module event.
+ * {@hide}
+ */
+oneway interface IIntResultListener {
+ void onResult(int resultCode);
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
new file mode 100644
index 0000000..cf094aa
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -0,0 +1,52 @@
+/*
+ * 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 android.net;
+
+import android.net.IIntResultListener;
+import android.net.ITetheringEventCallback;
+import android.net.TetheringRequestParcel;
+import android.os.ResultReceiver;
+
+/** @hide */
+oneway interface ITetheringConnector {
+ void tether(String iface, String callerPkg, String callingAttributionTag,
+ IIntResultListener receiver);
+
+ void untether(String iface, String callerPkg, String callingAttributionTag,
+ IIntResultListener receiver);
+
+ void setUsbTethering(boolean enable, String callerPkg,
+ String callingAttributionTag, IIntResultListener receiver);
+
+ void startTethering(in TetheringRequestParcel request, String callerPkg,
+ String callingAttributionTag, IIntResultListener receiver);
+
+ void stopTethering(int type, String callerPkg, String callingAttributionTag,
+ IIntResultListener receiver);
+
+ void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
+ boolean showEntitlementUi, String callerPkg, String callingAttributionTag);
+
+ void registerTetheringEventCallback(ITetheringEventCallback callback, String callerPkg);
+
+ void unregisterTetheringEventCallback(ITetheringEventCallback callback, String callerPkg);
+
+ void isTetheringSupported(String callerPkg, String callingAttributionTag,
+ IIntResultListener receiver);
+
+ void stopAllTethering(String callerPkg, String callingAttributionTag,
+ IIntResultListener receiver);
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
new file mode 100644
index 0000000..b4e3ba4
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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 android.net;
+
+import android.net.Network;
+import android.net.TetheredClient;
+import android.net.TetheringConfigurationParcel;
+import android.net.TetheringCallbackStartedParcel;
+import android.net.TetherStatesParcel;
+
+/**
+ * Callback class for receiving tethering changed events.
+ * @hide
+ */
+oneway interface ITetheringEventCallback
+{
+ /** Called immediately after the callbacks are registered */
+ void onCallbackStarted(in TetheringCallbackStartedParcel parcel);
+ void onCallbackStopped(int errorCode);
+ void onUpstreamChanged(in Network network);
+ void onConfigurationChanged(in TetheringConfigurationParcel config);
+ void onTetherStatesChanged(in TetherStatesParcel states);
+ void onTetherClientsChanged(in List<TetheredClient> clients);
+ void onOffloadStatusChanged(int status);
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
new file mode 100644
index 0000000..43262fb
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
@@ -0,0 +1,33 @@
+/*
+ * 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 android.net;
+
+import android.net.TetheringInterface;
+
+/**
+ * Status details for tethering downstream interfaces.
+ * {@hide}
+ */
+parcelable TetherStatesParcel {
+ TetheringInterface[] availableList;
+ TetheringInterface[] tetheredList;
+ TetheringInterface[] localOnlyList;
+ TetheringInterface[] erroredIfaceList;
+ // List of Last error code corresponding to each errored iface in erroredIfaceList. */
+ // TODO: Improve this as b/143122247.
+ int[] lastErrorList;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheredClient.aidl b/Tethering/common/TetheringLib/src/android/net/TetheredClient.aidl
new file mode 100644
index 0000000..0b279b8
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheredClient.aidl
@@ -0,0 +1,18 @@
+/**
+ * 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 android.net;
+
+@JavaOnlyStableParcelable parcelable TetheredClient;
\ No newline at end of file
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheredClient.java b/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
new file mode 100644
index 0000000..0b223f4
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
@@ -0,0 +1,239 @@
+/*
+ * 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 android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Information on a tethered downstream client.
+ * @hide
+ */
+@SystemApi
+public final class TetheredClient implements Parcelable {
+ @NonNull
+ private final MacAddress mMacAddress;
+ @NonNull
+ private final List<AddressInfo> mAddresses;
+ // TODO: use an @IntDef here
+ private final int mTetheringType;
+
+ public TetheredClient(@NonNull MacAddress macAddress,
+ @NonNull Collection<AddressInfo> addresses, int tetheringType) {
+ mMacAddress = macAddress;
+ mAddresses = new ArrayList<>(addresses);
+ mTetheringType = tetheringType;
+ }
+
+ private TetheredClient(@NonNull Parcel in) {
+ this(in.readParcelable(null), in.createTypedArrayList(AddressInfo.CREATOR), in.readInt());
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mMacAddress, flags);
+ dest.writeTypedList(mAddresses);
+ dest.writeInt(mTetheringType);
+ }
+
+ /**
+ * Get the MAC address used to identify the client.
+ */
+ @NonNull
+ public MacAddress getMacAddress() {
+ return mMacAddress;
+ }
+
+ /**
+ * Get information on the list of addresses that are associated with the client.
+ */
+ @NonNull
+ public List<AddressInfo> getAddresses() {
+ return new ArrayList<>(mAddresses);
+ }
+
+ /**
+ * Get the type of tethering used by the client.
+ * @return one of the {@code TetheringManager#TETHERING_*} constants.
+ */
+ public int getTetheringType() {
+ return mTetheringType;
+ }
+
+ /**
+ * Return a new {@link TetheredClient} that has all the attributes of this instance, plus the
+ * {@link AddressInfo} of the provided {@link TetheredClient}.
+ *
+ * <p>Duplicate addresses are removed.
+ * @hide
+ */
+ public TetheredClient addAddresses(@NonNull TetheredClient other) {
+ final LinkedHashSet<AddressInfo> newAddresses = new LinkedHashSet<>(
+ mAddresses.size() + other.mAddresses.size());
+ newAddresses.addAll(mAddresses);
+ newAddresses.addAll(other.mAddresses);
+ return new TetheredClient(mMacAddress, newAddresses, mTetheringType);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMacAddress, mAddresses, mTetheringType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof TetheredClient)) return false;
+ final TetheredClient other = (TetheredClient) obj;
+ return mMacAddress.equals(other.mMacAddress)
+ && mAddresses.equals(other.mAddresses)
+ && mTetheringType == other.mTetheringType;
+ }
+
+ /**
+ * Information on an lease assigned to a tethered client.
+ */
+ public static final class AddressInfo implements Parcelable {
+ @NonNull
+ private final LinkAddress mAddress;
+ @Nullable
+ private final String mHostname;
+
+ /** @hide */
+ public AddressInfo(@NonNull LinkAddress address, @Nullable String hostname) {
+ this.mAddress = address;
+ this.mHostname = hostname;
+ }
+
+ private AddressInfo(Parcel in) {
+ this(in.readParcelable(null), in.readString());
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mAddress, flags);
+ dest.writeString(mHostname);
+ }
+
+ /**
+ * Get the link address (including prefix length and lifetime) used by the client.
+ *
+ * This may be an IPv4 or IPv6 address.
+ */
+ @NonNull
+ public LinkAddress getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * Get the hostname that was advertised by the client when obtaining its address, if any.
+ */
+ @Nullable
+ public String getHostname() {
+ return mHostname;
+ }
+
+ /**
+ * Get the expiration time of the address assigned to the client.
+ * @hide
+ */
+ public long getExpirationTime() {
+ return mAddress.getExpirationTime();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAddress, mHostname);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof AddressInfo)) return false;
+ final AddressInfo other = (AddressInfo) obj;
+ // Use .equals() for addresses as all changes, including address expiry changes,
+ // should be included.
+ return other.mAddress.equals(mAddress)
+ && Objects.equals(mHostname, other.mHostname);
+ }
+
+ @NonNull
+ public static final Creator<AddressInfo> CREATOR = new Creator<AddressInfo>() {
+ @NonNull
+ @Override
+ public AddressInfo createFromParcel(@NonNull Parcel in) {
+ return new AddressInfo(in);
+ }
+
+ @NonNull
+ @Override
+ public AddressInfo[] newArray(int size) {
+ return new AddressInfo[size];
+ }
+ };
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "AddressInfo {"
+ + mAddress
+ + (mHostname != null ? ", hostname " + mHostname : "")
+ + "}";
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<TetheredClient> CREATOR = new Creator<TetheredClient>() {
+ @NonNull
+ @Override
+ public TetheredClient createFromParcel(@NonNull Parcel in) {
+ return new TetheredClient(in);
+ }
+
+ @NonNull
+ @Override
+ public TetheredClient[] newArray(int size) {
+ return new TetheredClient[size];
+ }
+ };
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "TetheredClient {hwAddr " + mMacAddress
+ + ", addresses " + mAddresses
+ + ", tetheringType " + mTetheringType
+ + "}";
+ }
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
new file mode 100644
index 0000000..253eacb
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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 android.net;
+
+import android.net.Network;
+import android.net.TetheredClient;
+import android.net.TetheringConfigurationParcel;
+import android.net.TetherStatesParcel;
+
+/**
+ * Initial information reported by tethering upon callback registration.
+ * @hide
+ */
+parcelable TetheringCallbackStartedParcel {
+ boolean tetheringSupported;
+ Network upstreamNetwork;
+ TetheringConfigurationParcel config;
+ TetherStatesParcel states;
+ List<TetheredClient> tetheredClients;
+ int offloadStatus;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl
new file mode 100644
index 0000000..89f3813
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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 android.net;
+
+/**
+ * Configuration details for tethering.
+ * @hide
+ */
+parcelable TetheringConfigurationParcel {
+ int subId;
+ String[] tetherableUsbRegexs;
+ String[] tetherableWifiRegexs;
+ String[] tetherableBluetoothRegexs;
+ boolean isDunRequired;
+ boolean chooseUpstreamAutomatically;
+ int[] preferredUpstreamIfaceTypes;
+ String[] legacyDhcpRanges;
+ String[] defaultIPv4DNS;
+ boolean enableLegacyDhcpServer;
+ String[] provisioningApp;
+ String provisioningAppNoUi;
+ int provisioningCheckPeriod;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringConstants.java b/Tethering/common/TetheringLib/src/android/net/TetheringConstants.java
new file mode 100644
index 0000000..f14def6
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringConstants.java
@@ -0,0 +1,93 @@
+/*
+ * 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 android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.SystemApi;
+import android.os.ResultReceiver;
+
+/**
+ * Collections of constants for internal tethering usage.
+ *
+ * <p>These hidden constants are not in TetheringManager as they are not part of the API stubs
+ * generated for TetheringManager, which prevents the tethering module from linking them at
+ * build time.
+ * TODO: investigate changing the tethering build rules so that Tethering can reference hidden
+ * symbols from framework-tethering even when they are in a non-hidden class.
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class TetheringConstants {
+ /** An explicit private class to avoid exposing constructor.*/
+ private TetheringConstants() { }
+
+ /**
+ * Extra used for communicating with the TetherService and TetherProvisioningActivity.
+ * Includes the type of tethering to enable if any.
+ */
+ public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType";
+ /**
+ * Extra used for communicating with the TetherService. Includes the type of tethering for
+ * which to cancel provisioning.
+ */
+ public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType";
+ /**
+ * Extra used for communicating with the TetherService. True to schedule a recheck of tether
+ * provisioning.
+ */
+ public static final String EXTRA_SET_ALARM = "extraSetAlarm";
+ /**
+ * Tells the TetherService to run a provision check now.
+ */
+ public static final String EXTRA_RUN_PROVISION = "extraRunProvision";
+ /**
+ * Extra used for communicating with the TetherService and TetherProvisioningActivity.
+ * Contains the {@link ResultReceiver} which will receive provisioning results.
+ * Can not be empty.
+ */
+ public static final String EXTRA_PROVISION_CALLBACK = "extraProvisionCallback";
+
+ /**
+ * Extra used for communicating with the TetherService and TetherProvisioningActivity.
+ * Contains the subId of current active cellular upstream.
+ * @hide
+ */
+ public static final String EXTRA_TETHER_SUBID = "android.net.extra.TETHER_SUBID";
+
+ /**
+ * Extra used for telling TetherProvisioningActivity the entitlement package name and class
+ * name to start UI entitlement check.
+ * @hide
+ */
+ public static final String EXTRA_TETHER_UI_PROVISIONING_APP_NAME =
+ "android.net.extra.TETHER_UI_PROVISIONING_APP_NAME";
+
+ /**
+ * Extra used for telling TetherService the intent action to start silent entitlement check.
+ * @hide
+ */
+ public static final String EXTRA_TETHER_SILENT_PROVISIONING_ACTION =
+ "android.net.extra.TETHER_SILENT_PROVISIONING_ACTION";
+
+ /**
+ * Extra used for TetherService to receive the response of provisioning check.
+ * @hide
+ */
+ public static final String EXTRA_TETHER_PROVISIONING_RESPONSE =
+ "android.net.extra.TETHER_PROVISIONING_RESPONSE";
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl
new file mode 100644
index 0000000..7151984
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl
@@ -0,0 +1,18 @@
+/*
+ * 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 android.net;
+
+@JavaOnlyStableParcelable parcelable TetheringInterface;
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
new file mode 100644
index 0000000..84cdef1
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.TetheringManager.TetheringType;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * The mapping of tethering interface and type.
+ * @hide
+ */
+@SystemApi
+public final class TetheringInterface implements Parcelable {
+ private final int mType;
+ private final String mInterface;
+
+ public TetheringInterface(@TetheringType int type, @NonNull String iface) {
+ Objects.requireNonNull(iface);
+ mType = type;
+ mInterface = iface;
+ }
+
+ private TetheringInterface(@NonNull Parcel in) {
+ this(in.readInt(), in.readString());
+ }
+
+ /** Get tethering type. */
+ public int getType() {
+ return mType;
+ }
+
+ /** Get tethering interface. */
+ @NonNull
+ public String getInterface() {
+ return mInterface;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeString(mInterface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mInterface);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof TetheringInterface)) return false;
+ final TetheringInterface other = (TetheringInterface) obj;
+ return mType == other.mType && mInterface.equals(other.mInterface);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<TetheringInterface> CREATOR = new Creator<TetheringInterface>() {
+ @NonNull
+ @Override
+ public TetheringInterface createFromParcel(@NonNull Parcel in) {
+ return new TetheringInterface(in);
+ }
+
+ @NonNull
+ @Override
+ public TetheringInterface[] newArray(int size) {
+ return new TetheringInterface[size];
+ }
+ };
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "TetheringInterface {mType=" + mType
+ + ", mInterface=" + mInterface + "}";
+ }
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
new file mode 100644
index 0000000..edd141d
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -0,0 +1,1541 @@
+/*
+ * 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 android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+
+/**
+ * This class provides the APIs to control the tethering service.
+ * <p> The primary responsibilities of this class are to provide the APIs for applications to
+ * start tethering, stop tethering, query configuration and query status.
+ *
+ * @hide
+ */
+@SystemApi
+public class TetheringManager {
+ 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;
+
+ @GuardedBy("mConnectorWaitQueue")
+ @Nullable
+ private ITetheringConnector mConnector;
+ @GuardedBy("mConnectorWaitQueue")
+ @NonNull
+ private final List<ConnectorConsumer> mConnectorWaitQueue = new ArrayList<>();
+ private final Supplier<IBinder> mConnectorSupplier;
+
+ private final TetheringCallbackInternal mCallback;
+ private final Context mContext;
+ private final ArrayMap<TetheringEventCallback, ITetheringEventCallback>
+ mTetheringEventCallbacks = new ArrayMap<>();
+
+ private volatile TetheringConfigurationParcel mTetheringConfiguration;
+ private volatile TetherStatesParcel mTetherStatesParcel;
+
+ /**
+ * Broadcast Action: A tetherable connection has come or gone.
+ * Uses {@code TetheringManager.EXTRA_AVAILABLE_TETHER},
+ * {@code TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY},
+ * {@code TetheringManager.EXTRA_ACTIVE_TETHER}, and
+ * {@code TetheringManager.EXTRA_ERRORED_TETHER} to indicate
+ * the current state of tethering. Each include a list of
+ * interface names in that state (may be empty).
+ *
+ * @deprecated New client should use TetheringEventCallback instead.
+ */
+ @Deprecated
+ public static final String ACTION_TETHER_STATE_CHANGED =
+ "android.net.conn.TETHER_STATE_CHANGED";
+
+ /**
+ * gives a String[] listing all the interfaces configured for
+ * tethering and currently available for tethering.
+ */
+ public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
+
+ /**
+ * gives a String[] listing all the interfaces currently in local-only
+ * mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
+ */
+ public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
+
+ /**
+ * gives a String[] listing all the interfaces currently tethered
+ * (ie, has DHCPv4 support and packets potentially forwarded/NATed)
+ */
+ public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
+
+ /**
+ * gives a String[] listing all the interfaces we tried to tether and
+ * failed. Use {@link #getLastTetherError} to find the error code
+ * for any interfaces listed here.
+ */
+ public static final String EXTRA_ERRORED_TETHER = "erroredArray";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, value = {
+ TETHERING_WIFI,
+ TETHERING_USB,
+ TETHERING_BLUETOOTH,
+ TETHERING_WIFI_P2P,
+ TETHERING_NCM,
+ TETHERING_ETHERNET,
+ })
+ public @interface TetheringType {
+ }
+
+ /**
+ * Invalid tethering type.
+ * @see #startTethering.
+ */
+ public static final int TETHERING_INVALID = -1;
+
+ /**
+ * Wifi tethering type.
+ * @see #startTethering.
+ */
+ public static final int TETHERING_WIFI = 0;
+
+ /**
+ * USB tethering type.
+ * @see #startTethering.
+ */
+ public static final int TETHERING_USB = 1;
+
+ /**
+ * Bluetooth tethering type.
+ * @see #startTethering.
+ */
+ public static final int TETHERING_BLUETOOTH = 2;
+
+ /**
+ * Wifi P2p tethering type.
+ * Wifi P2p tethering is set through events automatically, and don't
+ * need to start from #startTethering.
+ */
+ public static final int TETHERING_WIFI_P2P = 3;
+
+ /**
+ * Ncm local tethering type.
+ * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ */
+ public static final int TETHERING_NCM = 4;
+
+ /**
+ * Ethernet tethering type.
+ * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ */
+ public static final int TETHERING_ETHERNET = 5;
+
+ /**
+ * WIGIG tethering type. Use a separate type to prevent
+ * conflicts with TETHERING_WIFI
+ * This type is only used internally by the tethering module
+ * @hide
+ */
+ public static final int TETHERING_WIGIG = 6;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ TETHER_ERROR_NO_ERROR,
+ TETHER_ERROR_PROVISIONING_FAILED,
+ TETHER_ERROR_ENTITLEMENT_UNKNOWN,
+ })
+ public @interface EntitlementResult {
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ TETHER_ERROR_NO_ERROR,
+ TETHER_ERROR_UNKNOWN_IFACE,
+ TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHER_ERROR_INTERNAL_ERROR,
+ TETHER_ERROR_TETHER_IFACE_ERROR,
+ TETHER_ERROR_ENABLE_FORWARDING_ERROR,
+ TETHER_ERROR_DISABLE_FORWARDING_ERROR,
+ TETHER_ERROR_IFACE_CFG_ERROR,
+ TETHER_ERROR_DHCPSERVER_ERROR,
+ })
+ public @interface TetheringIfaceError {
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHER_ERROR_INTERNAL_ERROR,
+ TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
+ TETHER_ERROR_UNKNOWN_TYPE,
+ })
+ public @interface StartTetheringError {
+ }
+
+ public static final int TETHER_ERROR_NO_ERROR = 0;
+ public static final int TETHER_ERROR_UNKNOWN_IFACE = 1;
+ public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2;
+ public static final int TETHER_ERROR_UNSUPPORTED = 3;
+ public static final int TETHER_ERROR_UNAVAIL_IFACE = 4;
+ public static final int TETHER_ERROR_INTERNAL_ERROR = 5;
+ public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6;
+ public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7;
+ public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8;
+ public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9;
+ public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10;
+ public static final int TETHER_ERROR_PROVISIONING_FAILED = 11;
+ public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12;
+ public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13;
+ public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14;
+ public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15;
+ public static final int TETHER_ERROR_UNKNOWN_TYPE = 16;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, value = {
+ TETHER_HARDWARE_OFFLOAD_STOPPED,
+ TETHER_HARDWARE_OFFLOAD_STARTED,
+ TETHER_HARDWARE_OFFLOAD_FAILED,
+ })
+ public @interface TetherOffloadStatus {
+ }
+
+ /** Tethering offload status is stopped. */
+ public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0;
+ /** Tethering offload status is started. */
+ public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1;
+ /** Fail to start tethering offload. */
+ public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2;
+
+ /**
+ * Create a TetheringManager object for interacting with the tethering service.
+ *
+ * @param context Context for the manager.
+ * @param connectorSupplier Supplier for the manager connector; may return null while the
+ * service is not connected.
+ * {@hide}
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public TetheringManager(@NonNull final Context context,
+ @NonNull Supplier<IBinder> connectorSupplier) {
+ mContext = context;
+ mCallback = new TetheringCallbackInternal();
+ mConnectorSupplier = connectorSupplier;
+
+ final String pkgName = mContext.getOpPackageName();
+
+ final IBinder connector = mConnectorSupplier.get();
+ // If the connector is available on start, do not start a polling thread. This introduces
+ // differences in the thread that sends the oneway binder calls to the service between the
+ // first few seconds after boot and later, but it avoids always having differences between
+ // the first usage of TetheringManager from a process and subsequent usages (so the
+ // difference is only on boot). On boot binder calls may be queued until the service comes
+ // up and be sent from a worker thread; later, they are always sent from the caller thread.
+ // Considering that it's just oneway binder calls, and ordering is preserved, this seems
+ // better than inconsistent behavior persisting after boot.
+ if (connector != null) {
+ mConnector = ITetheringConnector.Stub.asInterface(connector);
+ } else {
+ startPollingForConnector();
+ }
+
+ Log.i(TAG, "registerTetheringEventCallback:" + pkgName);
+ getConnector(c -> c.registerTetheringEventCallback(mCallback, pkgName));
+ }
+
+ private void startPollingForConnector() {
+ new Thread(() -> {
+ while (true) {
+ try {
+ Thread.sleep(CONNECTOR_POLL_INTERVAL_MILLIS);
+ } catch (InterruptedException e) {
+ // Not much to do here, the system needs to wait for the connector
+ }
+
+ final IBinder connector = mConnectorSupplier.get();
+ if (connector != null) {
+ onTetheringConnected(ITetheringConnector.Stub.asInterface(connector));
+ return;
+ }
+ }
+ }).start();
+ }
+
+ private interface ConnectorConsumer {
+ void onConnectorAvailable(ITetheringConnector connector) throws RemoteException;
+ }
+
+ private void onTetheringConnected(ITetheringConnector connector) {
+ // Process the connector wait queue in order, including any items that are added
+ // while processing.
+ //
+ // 1. Copy the queue to a local variable under lock.
+ // 2. Drain the local queue with the lock released (otherwise, enqueuing future commands
+ // would block on the lock).
+ // 3. Acquire the lock again. If any new tasks were queued during step 2, goto 1.
+ // If not, set mConnector to non-null so future tasks are run immediately, not queued.
+ //
+ // For this to work, all calls to the tethering service must use getConnector(), which
+ // ensures that tasks are added to the queue with the lock held.
+ //
+ // Once mConnector is set to non-null, it will never be null again. If the network stack
+ // process crashes, no recovery is possible.
+ // TODO: evaluate whether it is possible to recover from network stack process crashes
+ // (though in most cases the system will have crashed when the network stack process
+ // crashes).
+ do {
+ final List<ConnectorConsumer> localWaitQueue;
+ synchronized (mConnectorWaitQueue) {
+ localWaitQueue = new ArrayList<>(mConnectorWaitQueue);
+ mConnectorWaitQueue.clear();
+ }
+
+ // Allow more tasks to be added at the end without blocking while draining the queue.
+ for (ConnectorConsumer task : localWaitQueue) {
+ try {
+ task.onConnectorAvailable(connector);
+ } catch (RemoteException e) {
+ // Most likely the network stack process crashed, which is likely to crash the
+ // system. Keep processing other requests but report the error loudly.
+ Log.wtf(TAG, "Error processing request for the tethering connector", e);
+ }
+ }
+
+ synchronized (mConnectorWaitQueue) {
+ if (mConnectorWaitQueue.size() == 0) {
+ mConnector = connector;
+ return;
+ }
+ }
+ } while (true);
+ }
+
+ /**
+ * Asynchronously get the ITetheringConnector to execute some operation.
+ *
+ * <p>If the connector is already available, the operation will be executed on the caller's
+ * thread. Otherwise it will be queued and executed on a worker thread. The operation should be
+ * limited to performing oneway binder calls to minimize differences due to threading.
+ */
+ private void getConnector(ConnectorConsumer consumer) {
+ final ITetheringConnector connector;
+ synchronized (mConnectorWaitQueue) {
+ connector = mConnector;
+ if (connector == null) {
+ mConnectorWaitQueue.add(consumer);
+ return;
+ }
+ }
+
+ try {
+ consumer.onConnectorAvailable(connector);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private interface RequestHelper {
+ void runRequest(ITetheringConnector connector, IIntResultListener listener);
+ }
+
+ // Used to dispatch legacy ConnectivityManager methods that expect tethering to be able to
+ // return results and perform operations synchronously.
+ // TODO: remove once there are no callers of these legacy methods.
+ private class RequestDispatcher {
+ private final ConditionVariable mWaiting;
+ public volatile int mRemoteResult;
+
+ private final IIntResultListener mListener = new IIntResultListener.Stub() {
+ @Override
+ public void onResult(final int resultCode) {
+ mRemoteResult = resultCode;
+ mWaiting.open();
+ }
+ };
+
+ RequestDispatcher() {
+ mWaiting = new ConditionVariable();
+ }
+
+ int waitForResult(final RequestHelper request) {
+ getConnector(c -> request.runRequest(c, mListener));
+ if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
+ throw new IllegalStateException("Callback timeout");
+ }
+
+ throwIfPermissionFailure(mRemoteResult);
+
+ return mRemoteResult;
+ }
+ }
+
+ private void throwIfPermissionFailure(final int errorCode) {
+ switch (errorCode) {
+ case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:
+ throw new SecurityException("No android.permission.TETHER_PRIVILEGED"
+ + " or android.permission.WRITE_SETTINGS permission");
+ case TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION:
+ throw new SecurityException(
+ "No android.permission.ACCESS_NETWORK_STATE permission");
+ }
+ }
+
+ private class TetheringCallbackInternal extends ITetheringEventCallback.Stub {
+ private volatile int mError = TETHER_ERROR_NO_ERROR;
+ private final ConditionVariable mWaitForCallback = new ConditionVariable();
+
+ @Override
+ public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
+ mTetheringConfiguration = parcel.config;
+ mTetherStatesParcel = parcel.states;
+ mWaitForCallback.open();
+ }
+
+ @Override
+ public void onCallbackStopped(int errorCode) {
+ mError = errorCode;
+ mWaitForCallback.open();
+ }
+
+ @Override
+ public void onUpstreamChanged(Network network) { }
+
+ @Override
+ public void onConfigurationChanged(TetheringConfigurationParcel config) {
+ mTetheringConfiguration = config;
+ }
+
+ @Override
+ public void onTetherStatesChanged(TetherStatesParcel states) {
+ mTetherStatesParcel = states;
+ }
+
+ @Override
+ public void onTetherClientsChanged(List<TetheredClient> clients) { }
+
+ @Override
+ public void onOffloadStatusChanged(int status) { }
+
+ public void waitForStarted() {
+ mWaitForCallback.block(DEFAULT_TIMEOUT_MS);
+ throwIfPermissionFailure(mError);
+ }
+ }
+
+ /**
+ * Attempt to tether the named interface. This will setup a dhcp server
+ * on the interface, forward and NAT IP v4 packets and forward DNS requests
+ * to the best active upstream network interface. Note that if no upstream
+ * IP network interface is available, dhcp will still run and traffic will be
+ * allowed between the tethered devices and this device, though upstream net
+ * access will of course fail until an upstream network interface becomes
+ * active.
+ *
+ * @deprecated The only usages is PanService. It uses this for legacy reasons
+ * and will migrate away as soon as possible.
+ *
+ * @param iface the interface name to tether.
+ * @return error a {@code TETHER_ERROR} value indicating success or failure type
+ *
+ * {@hide}
+ */
+ @Deprecated
+ @SystemApi(client = MODULE_LIBRARIES)
+ public int tether(@NonNull final String iface) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "tether caller:" + callerPkg);
+ final RequestDispatcher dispatcher = new RequestDispatcher();
+
+ return dispatcher.waitForResult((connector, listener) -> {
+ try {
+ connector.tether(iface, callerPkg, getAttributionTag(), listener);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ }
+
+ /**
+ * @return the context's attribution tag
+ */
+ private @Nullable String getAttributionTag() {
+ return mContext.getAttributionTag();
+ }
+
+ /**
+ * Stop tethering the named interface.
+ *
+ * @deprecated The only usages is PanService. It uses this for legacy reasons
+ * and will migrate away as soon as possible.
+ *
+ * {@hide}
+ */
+ @Deprecated
+ @SystemApi(client = MODULE_LIBRARIES)
+ public int untether(@NonNull final String iface) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "untether caller:" + callerPkg);
+
+ final RequestDispatcher dispatcher = new RequestDispatcher();
+
+ return dispatcher.waitForResult((connector, listener) -> {
+ try {
+ connector.untether(iface, callerPkg, getAttributionTag(), listener);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ }
+
+ /**
+ * Attempt to both alter the mode of USB and Tethering of USB.
+ *
+ * @deprecated New clients should not use this API anymore. All clients should use
+ * #startTethering or #stopTethering which encapsulate proper entitlement logic. If the API is
+ * used and an entitlement check is needed, downstream USB tethering will be enabled but will
+ * not have any upstream.
+ *
+ * {@hide}
+ */
+ @Deprecated
+ @SystemApi(client = MODULE_LIBRARIES)
+ public int setUsbTethering(final boolean enable) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "setUsbTethering caller:" + callerPkg);
+
+ final RequestDispatcher dispatcher = new RequestDispatcher();
+
+ return dispatcher.waitForResult((connector, listener) -> {
+ try {
+ connector.setUsbTethering(enable, callerPkg, getAttributionTag(),
+ listener);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ }
+
+ /**
+ * Indicates that this tethering connection will provide connectivity beyond this device (e.g.,
+ * global Internet access).
+ */
+ public static final int CONNECTIVITY_SCOPE_GLOBAL = 1;
+
+ /**
+ * Indicates that this tethering connection will only provide local connectivity.
+ */
+ public static final int CONNECTIVITY_SCOPE_LOCAL = 2;
+
+ /**
+ * Connectivity scopes for {@link TetheringRequest.Builder#setConnectivityScope}.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "CONNECTIVITY_SCOPE_", value = {
+ CONNECTIVITY_SCOPE_GLOBAL,
+ CONNECTIVITY_SCOPE_LOCAL,
+ })
+ public @interface ConnectivityScope {}
+
+ /**
+ * Use with {@link #startTethering} to specify additional parameters when starting tethering.
+ */
+ public static class TetheringRequest {
+ /** A configuration set for TetheringRequest. */
+ private final TetheringRequestParcel mRequestParcel;
+
+ private TetheringRequest(final TetheringRequestParcel request) {
+ mRequestParcel = request;
+ }
+
+ /** Builder used to create TetheringRequest. */
+ public static class Builder {
+ private final TetheringRequestParcel mBuilderParcel;
+
+ /** Default constructor of Builder. */
+ public Builder(@TetheringType final int type) {
+ mBuilderParcel = new TetheringRequestParcel();
+ mBuilderParcel.tetheringType = type;
+ mBuilderParcel.localIPv4Address = null;
+ mBuilderParcel.staticClientAddress = null;
+ mBuilderParcel.exemptFromEntitlementCheck = false;
+ mBuilderParcel.showProvisioningUi = true;
+ mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
+ }
+
+ /**
+ * Configure tethering with static IPv4 assignment.
+ *
+ * A DHCP server will be started, but will only be able to offer the client address.
+ * The two addresses must be in the same prefix.
+ *
+ * @param localIPv4Address The preferred local IPv4 link address to use.
+ * @param clientAddress The static client address.
+ */
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setStaticIpv4Addresses(@NonNull final LinkAddress localIPv4Address,
+ @NonNull final LinkAddress clientAddress) {
+ Objects.requireNonNull(localIPv4Address);
+ Objects.requireNonNull(clientAddress);
+ if (!checkStaticAddressConfiguration(localIPv4Address, clientAddress)) {
+ throw new IllegalArgumentException("Invalid server or client addresses");
+ }
+
+ mBuilderParcel.localIPv4Address = localIPv4Address;
+ mBuilderParcel.staticClientAddress = clientAddress;
+ return this;
+ }
+
+ /** Start tethering without entitlement checks. */
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setExemptFromEntitlementCheck(boolean exempt) {
+ mBuilderParcel.exemptFromEntitlementCheck = exempt;
+ return this;
+ }
+
+ /**
+ * If an entitlement check is needed, sets whether to show the entitlement UI or to
+ * perform a silent entitlement check. By default, the entitlement UI is shown.
+ */
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setShouldShowEntitlementUi(boolean showUi) {
+ mBuilderParcel.showProvisioningUi = showUi;
+ return this;
+ }
+
+ /**
+ * Sets the connectivity scope to be provided by this tethering downstream.
+ */
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setConnectivityScope(@ConnectivityScope int scope) {
+ if (!checkConnectivityScope(mBuilderParcel.tetheringType, scope)) {
+ throw new IllegalArgumentException("Invalid connectivity scope " + scope);
+ }
+
+ mBuilderParcel.connectivityScope = scope;
+ return this;
+ }
+
+ /** Build {@link TetheringRequest} with the currently set configuration. */
+ @NonNull
+ public TetheringRequest build() {
+ return new TetheringRequest(mBuilderParcel);
+ }
+ }
+
+ /**
+ * Get the local IPv4 address, if one was configured with
+ * {@link Builder#setStaticIpv4Addresses}.
+ */
+ @Nullable
+ public LinkAddress getLocalIpv4Address() {
+ return mRequestParcel.localIPv4Address;
+ }
+
+ /**
+ * Get the static IPv4 address of the client, if one was configured with
+ * {@link Builder#setStaticIpv4Addresses}.
+ */
+ @Nullable
+ public LinkAddress getClientStaticIpv4Address() {
+ return mRequestParcel.staticClientAddress;
+ }
+
+ /** Get tethering type. */
+ @TetheringType
+ public int getTetheringType() {
+ return mRequestParcel.tetheringType;
+ }
+
+ /** Get connectivity type */
+ @ConnectivityScope
+ public int getConnectivityScope() {
+ return mRequestParcel.connectivityScope;
+ }
+
+ /** Check if exempt from entitlement check. */
+ public boolean isExemptFromEntitlementCheck() {
+ return mRequestParcel.exemptFromEntitlementCheck;
+ }
+
+ /** Check if show entitlement ui. */
+ public boolean getShouldShowEntitlementUi() {
+ return mRequestParcel.showProvisioningUi;
+ }
+
+ /**
+ * Check whether the two addresses are ipv4 and in the same prefix.
+ * @hide
+ */
+ public static boolean checkStaticAddressConfiguration(
+ @NonNull final LinkAddress localIPv4Address,
+ @NonNull final LinkAddress clientAddress) {
+ return localIPv4Address.getPrefixLength() == clientAddress.getPrefixLength()
+ && localIPv4Address.isIpv4() && clientAddress.isIpv4()
+ && new IpPrefix(localIPv4Address.toString()).equals(
+ new IpPrefix(clientAddress.toString()));
+ }
+
+ /**
+ * Returns the default connectivity scope for the given tethering type. Usually this is
+ * CONNECTIVITY_SCOPE_GLOBAL, except for NCM which for historical reasons defaults to local.
+ * @hide
+ */
+ public static @ConnectivityScope int getDefaultConnectivityScope(int tetheringType) {
+ return tetheringType != TETHERING_NCM
+ ? CONNECTIVITY_SCOPE_GLOBAL
+ : CONNECTIVITY_SCOPE_LOCAL;
+ }
+
+ /**
+ * Checks whether the requested connectivity scope is allowed.
+ * @hide
+ */
+ private static boolean checkConnectivityScope(int type, int scope) {
+ if (scope == CONNECTIVITY_SCOPE_GLOBAL) return true;
+ return type == TETHERING_USB || type == TETHERING_ETHERNET || type == TETHERING_NCM;
+ }
+
+ /**
+ * Get a TetheringRequestParcel from the configuration
+ * @hide
+ */
+ public TetheringRequestParcel getParcel() {
+ return mRequestParcel;
+ }
+
+ /** String of TetheringRequest detail. */
+ public String toString() {
+ return "TetheringRequest [ type= " + mRequestParcel.tetheringType
+ + ", localIPv4Address= " + mRequestParcel.localIPv4Address
+ + ", staticClientAddress= " + mRequestParcel.staticClientAddress
+ + ", exemptFromEntitlementCheck= "
+ + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= "
+ + mRequestParcel.showProvisioningUi + " ]";
+ }
+ }
+
+ /**
+ * Callback for use with {@link #startTethering} to find out whether tethering succeeded.
+ */
+ public interface StartTetheringCallback {
+ /**
+ * Called when tethering has been successfully started.
+ */
+ default void onTetheringStarted() {}
+
+ /**
+ * Called when starting tethering failed.
+ *
+ * @param error The error that caused the failure.
+ */
+ default void onTetheringFailed(@StartTetheringError final int error) {}
+ }
+
+ /**
+ * Starts tethering and runs tether provisioning for the given type if needed. If provisioning
+ * fails, stopTethering will be called automatically.
+ *
+ * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
+ * fail if a tethering entitlement check is required.
+ *
+ * @param request a {@link TetheringRequest} which can specify the preferred configuration.
+ * @param executor {@link Executor} to specify the thread upon which the callback of
+ * TetheringRequest will be invoked.
+ * @param callback A callback that will be called to indicate the success status of the
+ * tethering start request.
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.TETHER_PRIVILEGED,
+ android.Manifest.permission.WRITE_SETTINGS
+ })
+ public void startTethering(@NonNull final TetheringRequest request,
+ @NonNull final Executor executor, @NonNull final StartTetheringCallback callback) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "startTethering caller:" + callerPkg);
+
+ final IIntResultListener listener = new IIntResultListener.Stub() {
+ @Override
+ public void onResult(final int resultCode) {
+ executor.execute(() -> {
+ if (resultCode == TETHER_ERROR_NO_ERROR) {
+ callback.onTetheringStarted();
+ } else {
+ callback.onTetheringFailed(resultCode);
+ }
+ });
+ }
+ };
+ getConnector(c -> c.startTethering(request.getParcel(), callerPkg,
+ getAttributionTag(), listener));
+ }
+
+ /**
+ * Starts tethering and runs tether provisioning for the given type if needed. If provisioning
+ * fails, stopTethering will be called automatically.
+ *
+ * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
+ * fail if a tethering entitlement check is required.
+ *
+ * @param type The tethering type, on of the {@code TetheringManager#TETHERING_*} constants.
+ * @param executor {@link Executor} to specify the thread upon which the callback of
+ * TetheringRequest will be invoked.
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.TETHER_PRIVILEGED,
+ android.Manifest.permission.WRITE_SETTINGS
+ })
+ @SystemApi(client = MODULE_LIBRARIES)
+ public void startTethering(int type, @NonNull final Executor executor,
+ @NonNull final StartTetheringCallback callback) {
+ startTethering(new TetheringRequest.Builder(type).build(), executor, callback);
+ }
+
+ /**
+ * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
+ * applicable.
+ *
+ * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
+ * fail if a tethering entitlement check is required.
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.TETHER_PRIVILEGED,
+ android.Manifest.permission.WRITE_SETTINGS
+ })
+ public void stopTethering(@TetheringType final int type) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "stopTethering caller:" + callerPkg);
+
+ getConnector(c -> c.stopTethering(type, callerPkg, getAttributionTag(),
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(int resultCode) {
+ // TODO: provide an API to obtain result
+ // This has never been possible as stopTethering has always been void and never
+ // taken a callback object. The only indication that callers have is if the call
+ // results in a TETHER_STATE_CHANGE broadcast.
+ }
+ }));
+ }
+
+ /**
+ * Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether
+ * entitlement succeeded.
+ */
+ public interface OnTetheringEntitlementResultListener {
+ /**
+ * Called to notify entitlement result.
+ *
+ * @param resultCode an int value of entitlement result. It may be one of
+ * {@link #TETHER_ERROR_NO_ERROR},
+ * {@link #TETHER_ERROR_PROVISIONING_FAILED}, or
+ * {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN}.
+ */
+ void onTetheringEntitlementResult(@EntitlementResult int result);
+ }
+
+ /**
+ * Request the latest value of the tethering entitlement check.
+ *
+ * <p>This method will only return the latest entitlement result if it is available. If no
+ * cached entitlement result is available, and {@code showEntitlementUi} is false,
+ * {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN} will be returned. If {@code showEntitlementUi} is
+ * true, entitlement will be run.
+ *
+ * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
+ * fail if a tethering entitlement check is required.
+ *
+ * @param type the downstream type of tethering. Must be one of {@code #TETHERING_*} constants.
+ * @param showEntitlementUi a boolean indicating whether to check result for the UI-based
+ * entitlement check or the silent entitlement check.
+ * @param executor the executor on which callback will be invoked.
+ * @param listener an {@link OnTetheringEntitlementResultListener} which will be called to
+ * notify the caller of the result of entitlement check. The listener may be called zero
+ * or one time.
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.TETHER_PRIVILEGED,
+ android.Manifest.permission.WRITE_SETTINGS
+ })
+ public void requestLatestTetheringEntitlementResult(@TetheringType int type,
+ boolean showEntitlementUi,
+ @NonNull Executor executor,
+ @NonNull final OnTetheringEntitlementResultListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException(
+ "OnTetheringEntitlementResultListener cannot be null.");
+ }
+
+ ResultReceiver wrappedListener = new ResultReceiver(null /* handler */) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ executor.execute(() -> {
+ listener.onTetheringEntitlementResult(resultCode);
+ });
+ }
+ };
+
+ requestLatestTetheringEntitlementResult(type, wrappedListener,
+ showEntitlementUi);
+ }
+
+ /**
+ * Helper function of #requestLatestTetheringEntitlementResult to remain backwards compatible
+ * with ConnectivityManager#getLatestTetheringEntitlementResult
+ *
+ * {@hide}
+ */
+ // TODO: improve the usage of ResultReceiver, b/145096122
+ @SystemApi(client = MODULE_LIBRARIES)
+ public void requestLatestTetheringEntitlementResult(@TetheringType final int type,
+ @NonNull final ResultReceiver receiver, final boolean showEntitlementUi) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "getLatestTetheringEntitlementResult caller:" + callerPkg);
+
+ getConnector(c -> c.requestLatestTetheringEntitlementResult(
+ type, receiver, showEntitlementUi, callerPkg, getAttributionTag()));
+ }
+
+ /**
+ * Callback for use with {@link registerTetheringEventCallback} to find out tethering
+ * upstream status.
+ */
+ public interface TetheringEventCallback {
+ /**
+ * Called when tethering supported status changed.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ *
+ * <p>Tethering may be disabled via system properties, device configuration, or device
+ * policy restrictions.
+ *
+ * @param supported The new supported status
+ */
+ default void onTetheringSupported(boolean supported) {}
+
+ /**
+ * Called when tethering upstream changed.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ *
+ * @param network the {@link Network} of tethering upstream. Null means tethering doesn't
+ * have any upstream.
+ */
+ default void onUpstreamChanged(@Nullable Network network) {}
+
+ /**
+ * Called when there was a change in tethering interface regular expressions.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param reg The new regular expressions.
+ *
+ * @deprecated New clients should use the callbacks with {@link TetheringInterface} which
+ * has the mapping between tethering type and interface. InterfaceRegex is no longer needed
+ * to determine the mapping of tethering type and interface.
+ *
+ * @hide
+ */
+ @Deprecated
+ @SystemApi(client = MODULE_LIBRARIES)
+ default void onTetherableInterfaceRegexpsChanged(@NonNull TetheringInterfaceRegexps reg) {}
+
+ /**
+ * Called when there was a change in the list of tetherable interfaces. Tetherable
+ * interface means this interface is available and can be used for tethering.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param interfaces The list of tetherable interface names.
+ */
+ default void onTetherableInterfacesChanged(@NonNull List<String> interfaces) {}
+
+ /**
+ * Called when there was a change in the list of tetherable interfaces. Tetherable
+ * interface means this interface is available and can be used for tethering.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param interfaces The set of TetheringInterface of currently tetherable interface.
+ */
+ default void onTetherableInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
+ // By default, the new callback calls the old callback, so apps
+ // implementing the old callback just work.
+ onTetherableInterfacesChanged(toIfaces(interfaces));
+ }
+
+ /**
+ * Called when there was a change in the list of tethered interfaces.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param interfaces The lit of 0 or more String of currently tethered interface names.
+ */
+ default void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {}
+
+ /**
+ * Called when there was a change in the list of tethered interfaces.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param interfaces The set of 0 or more TetheringInterface of currently tethered
+ * interface.
+ */
+ default void onTetheredInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
+ // By default, the new callback calls the old callback, so apps
+ // implementing the old callback just work.
+ onTetheredInterfacesChanged(toIfaces(interfaces));
+ }
+
+ /**
+ * Called when there was a change in the list of local-only interfaces.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param interfaces The list of 0 or more String of active local-only interface names.
+ */
+ default void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {}
+
+ /**
+ * Called when there was a change in the list of local-only interfaces.
+ *
+ * <p>This will be called immediately after the callback is registered, and may be called
+ * multiple times later upon changes.
+ * @param interfaces The set of 0 or more TetheringInterface of active local-only
+ * interface.
+ */
+ default void onLocalOnlyInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
+ // By default, the new callback calls the old callback, so apps
+ // implementing the old callback just work.
+ onLocalOnlyInterfacesChanged(toIfaces(interfaces));
+ }
+
+ /**
+ * Called when an error occurred configuring tethering.
+ *
+ * <p>This will be called immediately after the callback is registered if the latest status
+ * on the interface is an error, and may be called multiple times later upon changes.
+ * @param ifName Name of the interface.
+ * @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+ */
+ default void onError(@NonNull String ifName, @TetheringIfaceError int error) {}
+
+ /**
+ * Called when an error occurred configuring tethering.
+ *
+ * <p>This will be called immediately after the callback is registered if the latest status
+ * on the interface is an error, and may be called multiple times later upon changes.
+ * @param iface The interface that experienced the error.
+ * @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+ */
+ default void onError(@NonNull TetheringInterface iface, @TetheringIfaceError int error) {
+ // By default, the new callback calls the old callback, so apps
+ // implementing the old callback just work.
+ onError(iface.getInterface(), error);
+ }
+
+ /**
+ * Called when the list of tethered clients changes.
+ *
+ * <p>This callback provides best-effort information on connected clients based on state
+ * known to the system, however the list cannot be completely accurate (and should not be
+ * used for security purposes). For example, clients behind a bridge and using static IP
+ * assignments are not visible to the tethering device; or even when using DHCP, such
+ * clients may still be reported by this callback after disconnection as the system cannot
+ * determine if they are still connected.
+ * @param clients The new set of tethered clients; the collection is not ordered.
+ */
+ default void onClientsChanged(@NonNull Collection<TetheredClient> clients) {}
+
+ /**
+ * Called when tethering offload status changes.
+ *
+ * <p>This will be called immediately after the callback is registered.
+ * @param status The offload status.
+ */
+ default void onOffloadStatusChanged(@TetherOffloadStatus int status) {}
+ }
+
+ /**
+ * Covert DownStreamInterface collection to interface String array list. Internal use only.
+ *
+ * @hide
+ */
+ public static ArrayList<String> toIfaces(Collection<TetheringInterface> tetherIfaces) {
+ final ArrayList<String> ifaces = new ArrayList<>();
+ for (TetheringInterface tether : tetherIfaces) {
+ ifaces.add(tether.getInterface());
+ }
+
+ return ifaces;
+ }
+
+ private static String[] toIfaces(TetheringInterface[] tetherIfaces) {
+ final String[] ifaces = new String[tetherIfaces.length];
+ for (int i = 0; i < tetherIfaces.length; i++) {
+ ifaces[i] = tetherIfaces[i].getInterface();
+ }
+
+ return ifaces;
+ }
+
+
+ /**
+ * Regular expressions used to identify tethering interfaces.
+ *
+ * @deprecated Instead of using regex to determine tethering type. New client could use the
+ * callbacks with {@link TetheringInterface} which has the mapping of type and interface.
+ * @hide
+ */
+ @Deprecated
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static class TetheringInterfaceRegexps {
+ private final String[] mTetherableBluetoothRegexs;
+ private final String[] mTetherableUsbRegexs;
+ private final String[] mTetherableWifiRegexs;
+
+ /** @hide */
+ public TetheringInterfaceRegexps(@NonNull String[] tetherableBluetoothRegexs,
+ @NonNull String[] tetherableUsbRegexs, @NonNull String[] tetherableWifiRegexs) {
+ mTetherableBluetoothRegexs = tetherableBluetoothRegexs.clone();
+ mTetherableUsbRegexs = tetherableUsbRegexs.clone();
+ mTetherableWifiRegexs = tetherableWifiRegexs.clone();
+ }
+
+ @NonNull
+ public List<String> getTetherableBluetoothRegexs() {
+ return Collections.unmodifiableList(Arrays.asList(mTetherableBluetoothRegexs));
+ }
+
+ @NonNull
+ public List<String> getTetherableUsbRegexs() {
+ return Collections.unmodifiableList(Arrays.asList(mTetherableUsbRegexs));
+ }
+
+ @NonNull
+ public List<String> getTetherableWifiRegexs() {
+ return Collections.unmodifiableList(Arrays.asList(mTetherableWifiRegexs));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTetherableBluetoothRegexs, mTetherableUsbRegexs,
+ mTetherableWifiRegexs);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof TetheringInterfaceRegexps)) return false;
+ final TetheringInterfaceRegexps other = (TetheringInterfaceRegexps) obj;
+ return Arrays.equals(mTetherableBluetoothRegexs, other.mTetherableBluetoothRegexs)
+ && Arrays.equals(mTetherableUsbRegexs, other.mTetherableUsbRegexs)
+ && Arrays.equals(mTetherableWifiRegexs, other.mTetherableWifiRegexs);
+ }
+ }
+
+ /**
+ * Start listening to tethering change events. Any new added callback will receive the last
+ * tethering status right away. If callback is registered,
+ * {@link TetheringEventCallback#onUpstreamChanged} will immediately be called. If tethering
+ * has no upstream or disabled, the argument of callback will be null. The same callback object
+ * cannot be registered twice.
+ *
+ * @param executor the executor on which callback will be invoked.
+ * @param callback the callback to be called when tethering has change events.
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
+ public void registerTetheringEventCallback(@NonNull Executor executor,
+ @NonNull TetheringEventCallback callback) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "registerTetheringEventCallback caller:" + callerPkg);
+
+ synchronized (mTetheringEventCallbacks) {
+ if (mTetheringEventCallbacks.containsKey(callback)) {
+ throw new IllegalArgumentException("callback was already registered.");
+ }
+ final ITetheringEventCallback remoteCallback = new ITetheringEventCallback.Stub() {
+ // Only accessed with a lock on this object
+ private final HashMap<TetheringInterface, Integer> mErrorStates = new HashMap<>();
+ private TetheringInterface[] mLastTetherableInterfaces = null;
+ private TetheringInterface[] mLastTetheredInterfaces = null;
+ private TetheringInterface[] mLastLocalOnlyInterfaces = null;
+
+ @Override
+ public void onUpstreamChanged(Network network) throws RemoteException {
+ executor.execute(() -> {
+ callback.onUpstreamChanged(network);
+ });
+ }
+
+ private synchronized void sendErrorCallbacks(final TetherStatesParcel newStates) {
+ for (int i = 0; i < newStates.erroredIfaceList.length; i++) {
+ final TetheringInterface tetherIface = newStates.erroredIfaceList[i];
+ final Integer lastError = mErrorStates.get(tetherIface);
+ final int newError = newStates.lastErrorList[i];
+ if (newError != TETHER_ERROR_NO_ERROR
+ && !Objects.equals(lastError, newError)) {
+ callback.onError(tetherIface, newError);
+ }
+ mErrorStates.put(tetherIface, newError);
+ }
+ }
+
+ private synchronized void maybeSendTetherableIfacesChangedCallback(
+ final TetherStatesParcel newStates) {
+ if (Arrays.equals(mLastTetherableInterfaces, newStates.availableList)) return;
+ mLastTetherableInterfaces = newStates.availableList.clone();
+ callback.onTetherableInterfacesChanged(
+ Collections.unmodifiableSet((new ArraySet(mLastTetherableInterfaces))));
+ }
+
+ private synchronized void maybeSendTetheredIfacesChangedCallback(
+ final TetherStatesParcel newStates) {
+ if (Arrays.equals(mLastTetheredInterfaces, newStates.tetheredList)) return;
+ mLastTetheredInterfaces = newStates.tetheredList.clone();
+ callback.onTetheredInterfacesChanged(
+ Collections.unmodifiableSet((new ArraySet(mLastTetheredInterfaces))));
+ }
+
+ private synchronized void maybeSendLocalOnlyIfacesChangedCallback(
+ final TetherStatesParcel newStates) {
+ if (Arrays.equals(mLastLocalOnlyInterfaces, newStates.localOnlyList)) return;
+ mLastLocalOnlyInterfaces = newStates.localOnlyList.clone();
+ callback.onLocalOnlyInterfacesChanged(
+ Collections.unmodifiableSet((new ArraySet(mLastLocalOnlyInterfaces))));
+ }
+
+ // Called immediately after the callbacks are registered.
+ @Override
+ public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
+ executor.execute(() -> {
+ callback.onTetheringSupported(parcel.tetheringSupported);
+ callback.onUpstreamChanged(parcel.upstreamNetwork);
+ sendErrorCallbacks(parcel.states);
+ sendRegexpsChanged(parcel.config);
+ maybeSendTetherableIfacesChangedCallback(parcel.states);
+ maybeSendTetheredIfacesChangedCallback(parcel.states);
+ maybeSendLocalOnlyIfacesChangedCallback(parcel.states);
+ callback.onClientsChanged(parcel.tetheredClients);
+ callback.onOffloadStatusChanged(parcel.offloadStatus);
+ });
+ }
+
+ @Override
+ public void onCallbackStopped(int errorCode) {
+ executor.execute(() -> {
+ throwIfPermissionFailure(errorCode);
+ });
+ }
+
+ private void sendRegexpsChanged(TetheringConfigurationParcel parcel) {
+ callback.onTetherableInterfaceRegexpsChanged(new TetheringInterfaceRegexps(
+ parcel.tetherableBluetoothRegexs,
+ parcel.tetherableUsbRegexs,
+ parcel.tetherableWifiRegexs));
+ }
+
+ @Override
+ public void onConfigurationChanged(TetheringConfigurationParcel config) {
+ executor.execute(() -> sendRegexpsChanged(config));
+ }
+
+ @Override
+ public void onTetherStatesChanged(TetherStatesParcel states) {
+ executor.execute(() -> {
+ sendErrorCallbacks(states);
+ maybeSendTetherableIfacesChangedCallback(states);
+ maybeSendTetheredIfacesChangedCallback(states);
+ maybeSendLocalOnlyIfacesChangedCallback(states);
+ });
+ }
+
+ @Override
+ public void onTetherClientsChanged(final List<TetheredClient> clients) {
+ executor.execute(() -> callback.onClientsChanged(clients));
+ }
+
+ @Override
+ public void onOffloadStatusChanged(final int status) {
+ executor.execute(() -> callback.onOffloadStatusChanged(status));
+ }
+ };
+ getConnector(c -> c.registerTetheringEventCallback(remoteCallback, callerPkg));
+ mTetheringEventCallbacks.put(callback, remoteCallback);
+ }
+ }
+
+ /**
+ * Remove tethering event callback previously registered with
+ * {@link #registerTetheringEventCallback}.
+ *
+ * @param callback previously registered callback.
+ */
+ @RequiresPermission(anyOf = {
+ Manifest.permission.TETHER_PRIVILEGED,
+ Manifest.permission.ACCESS_NETWORK_STATE
+ })
+ public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "unregisterTetheringEventCallback caller:" + callerPkg);
+
+ synchronized (mTetheringEventCallbacks) {
+ ITetheringEventCallback remoteCallback = mTetheringEventCallbacks.remove(callback);
+ if (remoteCallback == null) {
+ throw new IllegalArgumentException("callback was not registered.");
+ }
+
+ getConnector(c -> c.unregisterTetheringEventCallback(remoteCallback, callerPkg));
+ }
+ }
+
+ /**
+ * Get a more detailed error code after a Tethering or Untethering
+ * request asynchronously failed.
+ *
+ * @param iface The name of the interface of interest
+ * @return error The error code of the last error tethering or untethering the named
+ * interface
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public int getLastTetherError(@NonNull final String iface) {
+ mCallback.waitForStarted();
+ if (mTetherStatesParcel == null) return TETHER_ERROR_NO_ERROR;
+
+ int i = 0;
+ for (TetheringInterface errored : mTetherStatesParcel.erroredIfaceList) {
+ if (iface.equals(errored.getInterface())) return mTetherStatesParcel.lastErrorList[i];
+
+ i++;
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ /**
+ * Get the list of regular expressions that define any tetherable
+ * USB network interfaces. If USB tethering is not supported by the
+ * device, this list should be empty.
+ *
+ * @return an array of 0 or more regular expression Strings defining
+ * what interfaces are considered tetherable usb interfaces.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public @NonNull String[] getTetherableUsbRegexs() {
+ mCallback.waitForStarted();
+ return mTetheringConfiguration.tetherableUsbRegexs;
+ }
+
+ /**
+ * Get the list of regular expressions that define any tetherable
+ * Wifi network interfaces. If Wifi tethering is not supported by the
+ * device, this list should be empty.
+ *
+ * @return an array of 0 or more regular expression Strings defining
+ * what interfaces are considered tetherable wifi interfaces.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public @NonNull String[] getTetherableWifiRegexs() {
+ mCallback.waitForStarted();
+ return mTetheringConfiguration.tetherableWifiRegexs;
+ }
+
+ /**
+ * Get the list of regular expressions that define any tetherable
+ * Bluetooth network interfaces. If Bluetooth tethering is not supported by the
+ * device, this list should be empty.
+ *
+ * @return an array of 0 or more regular expression Strings defining
+ * what interfaces are considered tetherable bluetooth interfaces.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public @NonNull String[] getTetherableBluetoothRegexs() {
+ mCallback.waitForStarted();
+ return mTetheringConfiguration.tetherableBluetoothRegexs;
+ }
+
+ /**
+ * Get the set of tetherable, available interfaces. This list is limited by
+ * device configuration and current interface existence.
+ *
+ * @return an array of 0 or more Strings of tetherable interface names.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public @NonNull String[] getTetherableIfaces() {
+ mCallback.waitForStarted();
+ if (mTetherStatesParcel == null) return new String[0];
+
+ return toIfaces(mTetherStatesParcel.availableList);
+ }
+
+ /**
+ * Get the set of tethered interfaces.
+ *
+ * @return an array of 0 or more String of currently tethered interface names.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public @NonNull String[] getTetheredIfaces() {
+ mCallback.waitForStarted();
+ if (mTetherStatesParcel == null) return new String[0];
+
+ return toIfaces(mTetherStatesParcel.tetheredList);
+ }
+
+ /**
+ * Get the set of interface names which attempted to tether but
+ * failed. Re-attempting to tether may cause them to reset to the Tethered
+ * state. Alternatively, causing the interface to be destroyed and recreated
+ * may cause them to reset to the available state.
+ * {@link TetheringManager#getLastTetherError} can be used to get more
+ * information on the cause of the errors.
+ *
+ * @return an array of 0 or more String indicating the interface names
+ * which failed to tether.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public @NonNull String[] getTetheringErroredIfaces() {
+ mCallback.waitForStarted();
+ if (mTetherStatesParcel == null) return new String[0];
+
+ return toIfaces(mTetherStatesParcel.erroredIfaceList);
+ }
+
+ /**
+ * Get the set of tethered dhcp ranges.
+ *
+ * @deprecated This API just return the default value which is not used in DhcpServer.
+ * @hide
+ */
+ @Deprecated
+ public @NonNull String[] getTetheredDhcpRanges() {
+ mCallback.waitForStarted();
+ return mTetheringConfiguration.legacyDhcpRanges;
+ }
+
+ /**
+ * Check if the device allows for tethering. It may be disabled via
+ * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or
+ * due to device configuration.
+ *
+ * @return a boolean - {@code true} indicating Tethering is supported.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public boolean isTetheringSupported() {
+ final String callerPkg = mContext.getOpPackageName();
+
+ return isTetheringSupported(callerPkg);
+ }
+
+ /**
+ * Check if the device allows for tethering. It may be disabled via {@code ro.tether.denied}
+ * system property, Settings.TETHER_SUPPORTED or due to device configuration. This is useful
+ * for system components that query this API on behalf of an app. In particular, Bluetooth
+ * has @UnsupportedAppUsage calls that will let apps turn on bluetooth tethering if they have
+ * the right permissions, but such an app needs to know whether it can (permissions as well
+ * as support from the device) turn on tethering in the first place to show the appropriate UI.
+ *
+ * @param callerPkg The caller package name, if it is not matching the calling uid,
+ * SecurityException would be thrown.
+ * @return a boolean - {@code true} indicating Tethering is supported.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public boolean isTetheringSupported(@NonNull final String callerPkg) {
+
+ final RequestDispatcher dispatcher = new RequestDispatcher();
+ final int ret = dispatcher.waitForResult((connector, listener) -> {
+ try {
+ connector.isTetheringSupported(callerPkg, getAttributionTag(), listener);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+
+ return ret == TETHER_ERROR_NO_ERROR;
+ }
+
+ /**
+ * Stop all active tethering.
+ *
+ * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
+ * fail if a tethering entitlement check is required.
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.TETHER_PRIVILEGED,
+ android.Manifest.permission.WRITE_SETTINGS
+ })
+ public void stopAllTethering() {
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "stopAllTethering caller:" + callerPkg);
+
+ getConnector(c -> c.stopAllTethering(callerPkg, getAttributionTag(),
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(int resultCode) {
+ // TODO: add an API parameter to send result to caller.
+ // This has never been possible as stopAllTethering has always been void
+ // and never taken a callback object. The only indication that callers have
+ // is if the call results in a TETHER_STATE_CHANGE broadcast.
+ }
+ }));
+ }
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
new file mode 100644
index 0000000..f13c970
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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 android.net;
+
+import android.net.LinkAddress;
+
+/**
+ * Configuration details for requesting tethering.
+ * @hide
+ */
+parcelable TetheringRequestParcel {
+ int tetheringType;
+ LinkAddress localIPv4Address;
+ LinkAddress staticClientAddress;
+ boolean exemptFromEntitlementCheck;
+ boolean showProvisioningUi;
+ int connectivityScope;
+}
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
new file mode 100644
index 0000000..5de4b97
--- /dev/null
+++ b/Tethering/jarjar-rules.txt
@@ -0,0 +1,14 @@
+# These must be kept in sync with the framework-connectivity-shared-srcs filegroup.
+# Classes from the framework-connectivity-shared-srcs filegroup.
+# If there are files in that filegroup that are not covered below, the classes in the
+# module will be overwritten by the ones in the framework.
+rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
+rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
+
+rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1
+
+# Classes from net-utils-framework-common
+rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1
+
+# Classes from net-utils-device-common
+rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1
diff --git a/Tethering/jni/android_net_util_TetheringUtils.cpp b/Tethering/jni/android_net_util_TetheringUtils.cpp
new file mode 100644
index 0000000..27c84cf
--- /dev/null
+++ b/Tethering/jni/android_net_util_TetheringUtils.cpp
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <errno.h>
+#include <error.h>
+#include <jni.h>
+#include <linux/filter.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <netjniutils/netjniutils.h>
+#include <net/if.h>
+#include <netinet/ether.h>
+#include <netinet/ip6.h>
+#include <netinet/icmp6.h>
+#include <sys/socket.h>
+#include <stdio.h>
+
+namespace android {
+
+static const uint32_t kIPv6NextHeaderOffset = offsetof(ip6_hdr, ip6_nxt);
+static const uint32_t kIPv6PayloadStart = sizeof(ip6_hdr);
+static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type);
+
+static void android_net_util_setupIcmpFilter(JNIEnv *env, jobject javaFd, uint32_t type) {
+ sock_filter filter_code[] = {
+ // Check header is ICMPv6.
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv6NextHeaderOffset),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3),
+
+ // Check ICMPv6 type.
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kICMPv6TypeOffset),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, type, 0, 1),
+
+ // Accept or reject.
+ BPF_STMT(BPF_RET | BPF_K, 0xffff),
+ BPF_STMT(BPF_RET | BPF_K, 0)
+ };
+
+ const sock_fprog filter = {
+ sizeof(filter_code) / sizeof(filter_code[0]),
+ filter_code,
+ };
+
+ int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
+ if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+ }
+}
+
+static void android_net_util_setupNaSocket(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+ android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_ADVERT);
+}
+
+static void android_net_util_setupNsSocket(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+ android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_SOLICIT);
+}
+
+static void android_net_util_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
+ jint ifIndex)
+{
+ static const int kLinkLocalHopLimit = 255;
+
+ int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
+
+ // Set an ICMPv6 filter that only passes Router Solicitations.
+ struct icmp6_filter rs_only;
+ ICMP6_FILTER_SETBLOCKALL(&rs_only);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &rs_only);
+ socklen_t len = sizeof(rs_only);
+ if (setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &rs_only, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(ICMP6_FILTER): %s", strerror(errno));
+ return;
+ }
+
+ // Most/all of the rest of these options can be set via Java code, but
+ // because we're here on account of setting an icmp6_filter go ahead
+ // and do it all natively for now.
+
+ // Set the multicast hoplimit to 255 (link-local only).
+ int hops = kLinkLocalHopLimit;
+ len = sizeof(hops);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_MULTICAST_HOPS): %s", strerror(errno));
+ return;
+ }
+
+ // Set the unicast hoplimit to 255 (link-local only).
+ hops = kLinkLocalHopLimit;
+ len = sizeof(hops);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_UNICAST_HOPS): %s", strerror(errno));
+ return;
+ }
+
+ // Explicitly disable multicast loopback.
+ int off = 0;
+ len = sizeof(off);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &off, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_MULTICAST_LOOP): %s", strerror(errno));
+ return;
+ }
+
+ // Specify the IPv6 interface to use for outbound multicast.
+ len = sizeof(ifIndex);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifIndex, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_MULTICAST_IF): %s", strerror(errno));
+ return;
+ }
+
+ // Additional options to be considered:
+ // - IPV6_TCLASS
+ // - IPV6_RECVPKTINFO
+ // - IPV6_RECVHOPLIMIT
+
+ // Bind to [::].
+ const struct sockaddr_in6 sin6 = {
+ .sin6_family = AF_INET6,
+ .sin6_port = 0,
+ .sin6_flowinfo = 0,
+ .sin6_addr = IN6ADDR_ANY_INIT,
+ .sin6_scope_id = 0,
+ };
+ auto sa = reinterpret_cast<const struct sockaddr *>(&sin6);
+ len = sizeof(sin6);
+ if (bind(fd, sa, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "bind(IN6ADDR_ANY): %s", strerror(errno));
+ return;
+ }
+
+ // Join the all-routers multicast group, ff02::2%index.
+ struct ipv6_mreq all_rtrs = {
+ .ipv6mr_multiaddr = {{{0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2}}},
+ .ipv6mr_interface = ifIndex,
+ };
+ len = sizeof(all_rtrs);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &all_rtrs, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_JOIN_GROUP): %s", strerror(errno));
+ return;
+ }
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "setupNaSocket", "(Ljava/io/FileDescriptor;)V",
+ (void*) android_net_util_setupNaSocket },
+ { "setupNsSocket", "(Ljava/io/FileDescriptor;)V",
+ (void*) android_net_util_setupNsSocket },
+ { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V",
+ (void*) android_net_util_setupRaSocket },
+};
+
+int register_android_net_util_TetheringUtils(JNIEnv* env) {
+ return jniRegisterNativeMethods(env,
+ "android/net/util/TetheringUtils",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp
new file mode 100644
index 0000000..27357f8
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "bpf_tethering.h"
+
+namespace android {
+
+static jobjectArray getBpfCounterNames(JNIEnv *env) {
+ size_t size = BPF_TETHER_ERR__MAX;
+ jobjectArray ret = env->NewObjectArray(size, env->FindClass("java/lang/String"), nullptr);
+ for (int i = 0; i < size; i++) {
+ env->SetObjectArrayElement(ret, i, env->NewStringUTF(bpf_tether_errors[i]));
+ }
+ return ret;
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "getBpfCounterNames", "()[Ljava/lang/String;", (void*) getBpfCounterNames },
+};
+
+int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env) {
+ return jniRegisterNativeMethods(env,
+ "com/android/networkstack/tethering/BpfCoordinator",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp
new file mode 100644
index 0000000..eadc210
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ */
+
+#include <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "nativehelper/scoped_primitive_array.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+
+namespace android {
+
+static jclass sErrnoExceptionClass;
+static jmethodID sErrnoExceptionCtor2;
+static jmethodID sErrnoExceptionCtor3;
+
+static void throwErrnoException(JNIEnv* env, const char* functionName, int error) {
+ if (sErrnoExceptionClass == nullptr || sErrnoExceptionClass == nullptr) return;
+
+ jthrowable cause = nullptr;
+ if (env->ExceptionCheck()) {
+ cause = env->ExceptionOccurred();
+ env->ExceptionClear();
+ }
+
+ ScopedLocalRef<jstring> msg(env, env->NewStringUTF(functionName));
+
+ // Not really much we can do here if msg is null, let's try to stumble on...
+ if (msg.get() == nullptr) env->ExceptionClear();
+
+ jobject errnoException;
+ if (cause != nullptr) {
+ errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor3, msg.get(),
+ error, cause);
+ } else {
+ errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor2, msg.get(),
+ error);
+ }
+ env->Throw(static_cast<jthrowable>(errnoException));
+}
+
+static jint com_android_networkstack_tethering_BpfMap_closeMap(JNIEnv *env, jobject clazz,
+ jint fd) {
+ int ret = close(fd);
+
+ if (ret) throwErrnoException(env, "closeMap", errno);
+
+ return ret;
+}
+
+static jint com_android_networkstack_tethering_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz,
+ jstring path, jint mode) {
+ ScopedUtfChars pathname(env, path);
+
+ jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
+
+ return fd;
+}
+
+static void com_android_networkstack_tethering_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray value, jint flags) {
+ ScopedByteArrayRO keyRO(env, key);
+ ScopedByteArrayRO valueRO(env, value);
+
+ int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
+ static_cast<int>(flags));
+
+ if (ret) throwErrnoException(env, "writeToMapEntry", errno);
+}
+
+static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
+ if (ret == 0) return true;
+
+ if (err != ENOENT) throwErrnoException(env, functionName, err);
+ return false;
+}
+
+static jboolean com_android_networkstack_tethering_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key) {
+ ScopedByteArrayRO keyRO(env, key);
+
+ // On success, zero is returned. If the element is not found, -1 is returned and errno is set
+ // to ENOENT.
+ int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
+
+ return throwIfNotEnoent(env, "deleteMapEntry", ret, errno);
+}
+
+static jboolean com_android_networkstack_tethering_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray nextKey) {
+ // If key is found, the operation returns zero and sets the next key pointer to the key of the
+ // next element. If key is not found, the operation returns zero and sets the next key pointer
+ // to the key of the first element. If key is the last element, -1 is returned and errno is
+ // set to ENOENT. Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL.
+ ScopedByteArrayRW nextKeyRW(env, nextKey);
+ int ret;
+ if (key == nullptr) {
+ // Called by getFirstKey. Find the first key in the map.
+ ret = bpf::getNextMapKey(static_cast<int>(fd), nullptr, nextKeyRW.get());
+ } else {
+ ScopedByteArrayRO keyRO(env, key);
+ ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
+ }
+
+ return throwIfNotEnoent(env, "getNextMapKey", ret, errno);
+}
+
+static jboolean com_android_networkstack_tethering_BpfMap_findMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray value) {
+ ScopedByteArrayRO keyRO(env, key);
+ ScopedByteArrayRW valueRW(env, value);
+
+ // If an element is found, the operation returns zero and stores the element's value into
+ // "value". If no element is found, the operation returns -1 and sets errno to ENOENT.
+ int ret = bpf::findMapEntry(static_cast<int>(fd), keyRO.get(), valueRW.get());
+
+ return throwIfNotEnoent(env, "findMapEntry", ret, errno);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "closeMap", "(I)I",
+ (void*) com_android_networkstack_tethering_BpfMap_closeMap },
+ { "bpfFdGet", "(Ljava/lang/String;I)I",
+ (void*) com_android_networkstack_tethering_BpfMap_bpfFdGet },
+ { "writeToMapEntry", "(I[B[BI)V",
+ (void*) com_android_networkstack_tethering_BpfMap_writeToMapEntry },
+ { "deleteMapEntry", "(I[B)Z",
+ (void*) com_android_networkstack_tethering_BpfMap_deleteMapEntry },
+ { "getNextMapKey", "(I[B[B)Z",
+ (void*) com_android_networkstack_tethering_BpfMap_getNextMapKey },
+ { "findMapEntry", "(I[B[B)Z",
+ (void*) com_android_networkstack_tethering_BpfMap_findMapEntry },
+
+};
+
+int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env) {
+ sErrnoExceptionClass = static_cast<jclass>(env->NewGlobalRef(
+ env->FindClass("android/system/ErrnoException")));
+ if (sErrnoExceptionClass == nullptr) return JNI_ERR;
+
+ sErrnoExceptionCtor2 = env->GetMethodID(sErrnoExceptionClass, "<init>",
+ "(Ljava/lang/String;I)V");
+ if (sErrnoExceptionCtor2 == nullptr) return JNI_ERR;
+
+ sErrnoExceptionCtor3 = env->GetMethodID(sErrnoExceptionClass, "<init>",
+ "(Ljava/lang/String;ILjava/lang/Throwable;)V");
+ if (sErrnoExceptionCtor3 == nullptr) return JNI_ERR;
+
+ return jniRegisterNativeMethods(env,
+ "com/android/networkstack/tethering/BpfMap",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
new file mode 100644
index 0000000..1611f9d
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
@@ -0,0 +1,352 @@
+/*
+ * 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 <arpa/inet.h>
+#include <jni.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <linux/rtnetlink.h>
+#include <nativehelper/JNIHelp.h>
+#include <net/if.h>
+#include <stdio.h>
+#include <sys/socket.h>
+
+// TODO: use unique_fd.
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+#include "bpf_tethering.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
+#define CLS_BPF_NAME_LEN 256
+
+// Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
+#define CLS_BPF_KIND_NAME "bpf"
+
+namespace android {
+// Sync from system/netd/server/NetlinkCommands.h
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+
+// TODO: move to frameworks/libs/net/common/native for sharing with
+// system/netd/server/OffloadUtils.{c, h}.
+static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) {
+ int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); // TODO: use unique_fd
+ if (fd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %s",
+ strerror(errno));
+ return;
+ }
+
+ static constexpr int on = 1;
+ if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
+ close(fd);
+ return;
+ }
+
+ // this is needed to get valid strace netlink parsing, it allocates the pid
+ if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "bind(fd, {AF_NETLINK, 0, 0}): %s",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ // we do not want to receive messages from anyone besides the kernel
+ if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "connect(fd, {AF_NETLINK, 0, 0}): %s",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ int rv = send(fd, req, len, 0);
+
+ if (rv == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ if (rv != len) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s",
+ strerror(EMSGSIZE));
+ close(fd);
+ return;
+ }
+
+ struct {
+ nlmsghdr h;
+ nlmsgerr e;
+ char buf[256];
+ } resp = {};
+
+ rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+ if (rv == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "recv() failed: %s", strerror(errno));
+ close(fd);
+ return;
+ }
+
+ if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv);
+ close(fd);
+ return;
+ }
+
+ if (resp.h.nlmsg_len != (unsigned)rv) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len,
+ rv);
+ close(fd);
+ return;
+ }
+
+ if (resp.h.nlmsg_type != NLMSG_ERROR) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
+ close(fd);
+ return;
+ }
+
+ if (resp.e.error) { // returns 0 on success
+ jniThrowExceptionFmt(env, "java/io/IOException", "NLMSG_ERROR message return error: %s",
+ strerror(-resp.e.error));
+ }
+ close(fd);
+ return;
+}
+
+static int hardwareAddressType(const char* interface) {
+ int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (fd < 0) return -errno;
+
+ struct ifreq ifr = {};
+ // We use strncpy() instead of strlcpy() since kernel has to be able
+ // to handle non-zero terminated junk passed in by userspace anyway,
+ // and this way too long interface names (more than IFNAMSIZ-1 = 15
+ // characters plus terminating NULL) will not get truncated to 15
+ // characters and zero-terminated and thus potentially erroneously
+ // match a truncated interface if one were to exist.
+ strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name));
+
+ int rv;
+ if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) {
+ rv = -errno;
+ } else {
+ rv = ifr.ifr_hwaddr.sa_family;
+ }
+
+ close(fd);
+ return rv;
+}
+
+static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz,
+ jstring iface) {
+ ScopedUtfChars interface(env, iface);
+
+ int rv = hardwareAddressType(interface.c_str());
+ if (rv < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "Get hardware address type of interface %s failed: %s",
+ interface.c_str(), strerror(-rv));
+ return false;
+ }
+
+ switch (rv) {
+ case ARPHRD_ETHER:
+ return true;
+ case ARPHRD_NONE:
+ case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519
+ case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
+ return false;
+ default:
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "Unknown hardware address type %s on interface %s", rv,
+ interface.c_str());
+ return false;
+ }
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf(
+ JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto,
+ jstring bpfProgPath) {
+ ScopedUtfChars pathname(env, bpfProgPath);
+
+ const int bpfFd = bpf::retrieveProgram(pathname.c_str());
+ if (bpfFd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "retrieveProgram failed %s",
+ strerror(errno));
+ return;
+ }
+
+ struct {
+ nlmsghdr n;
+ tcmsg t;
+ struct {
+ nlattr attr;
+ // The maximum classifier name length is defined in
+ // tcf_proto_ops in include/net/sch_generic.h.
+ char str[NLMSG_ALIGN(sizeof(CLS_BPF_KIND_NAME))];
+ } kind;
+ struct {
+ nlattr attr;
+ struct {
+ nlattr attr;
+ __u32 u32;
+ } fd;
+ struct {
+ nlattr attr;
+ char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+ } name;
+ struct {
+ nlattr attr;
+ __u32 u32;
+ } flags;
+ } options;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_NEWTFILTER,
+ .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_UNSPEC,
+ .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+ ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+ .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+ htons(static_cast<uint16_t>(proto))),
+ },
+ .kind =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.kind),
+ .nla_type = TCA_KIND,
+ },
+ .str = CLS_BPF_KIND_NAME,
+ },
+ .options =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options),
+ .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+ },
+ .fd =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.fd),
+ .nla_type = TCA_BPF_FD,
+ },
+ .u32 = static_cast<__u32>(bpfFd),
+ },
+ .name =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.name),
+ .nla_type = TCA_BPF_NAME,
+ },
+ // Visible via 'tc filter show', but
+ // is overwritten by strncpy below
+ .str = "placeholder",
+ },
+ .flags =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.flags),
+ .nla_type = TCA_BPF_FLAGS,
+ },
+ .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+ },
+ },
+ };
+
+ snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
+ basename(pathname.c_str()));
+
+ // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of
+ // BPF program before returning the function in any case.
+ sendAndProcessNetlinkResponse(env, &req, sizeof(req));
+ close(bpfFd);
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz,
+ jint ifIndex,
+ jboolean ingress,
+ jshort prio, jshort proto) {
+ const struct {
+ nlmsghdr n;
+ tcmsg t;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_DELTFILTER,
+ .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_UNSPEC,
+ .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+ ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+ .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+ htons(static_cast<uint16_t>(proto))),
+ },
+ };
+
+ sendAndProcessNetlinkResponse(env, &req, sizeof(req));
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"isEthernet", "(Ljava/lang/String;)Z",
+ (void*)com_android_networkstack_tethering_BpfUtils_isEthernet},
+ {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V",
+ (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf},
+ {"tcFilterDelDev", "(IZSS)V",
+ (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev},
+};
+
+int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods,
+ NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp
new file mode 100644
index 0000000..02e602d
--- /dev/null
+++ b/Tethering/jni/onload.cpp
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include "jni.h"
+
+#define LOG_TAG "TetheringJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_android_net_util_TetheringUtils(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_android_net_util_TetheringUtils(env) < 0) return JNI_ERR;
+
+ if (register_com_android_networkstack_tethering_BpfMap(env) < 0) return JNI_ERR;
+
+ if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR;
+
+ if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
new file mode 100644
index 0000000..75ecdce
--- /dev/null
+++ b/Tethering/proguard.flags
@@ -0,0 +1,17 @@
+# Keep class's integer static field for MessageUtils to parsing their name.
+-keep class com.android.networkstack.tethering.Tethering$TetherMainSM {
+ static final int CMD_*;
+ static final int EVENT_*;
+}
+
+-keep class com.android.networkstack.tethering.BpfMap {
+ native <methods>;
+}
+
+-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
+ *;
+}
+
+-keepclassmembers class android.net.ip.IpServer {
+ static final int CMD_*;
+}
diff --git a/Tethering/res/drawable-hdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-hdpi/stat_sys_tether_bluetooth.png
new file mode 100644
index 0000000..9451174
--- /dev/null
+++ b/Tethering/res/drawable-hdpi/stat_sys_tether_bluetooth.png
Binary files differ
diff --git a/Tethering/res/drawable-hdpi/stat_sys_tether_general.png b/Tethering/res/drawable-hdpi/stat_sys_tether_general.png
new file mode 100644
index 0000000..79d5756
--- /dev/null
+++ b/Tethering/res/drawable-hdpi/stat_sys_tether_general.png
Binary files differ
diff --git a/Tethering/res/drawable-hdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-hdpi/stat_sys_tether_usb.png
new file mode 100644
index 0000000..cae1bd1
--- /dev/null
+++ b/Tethering/res/drawable-hdpi/stat_sys_tether_usb.png
Binary files differ
diff --git a/Tethering/res/drawable-ldpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-ldpi/stat_sys_tether_bluetooth.png
new file mode 100644
index 0000000..ffe8e8c
--- /dev/null
+++ b/Tethering/res/drawable-ldpi/stat_sys_tether_bluetooth.png
Binary files differ
diff --git a/Tethering/res/drawable-ldpi/stat_sys_tether_general.png b/Tethering/res/drawable-ldpi/stat_sys_tether_general.png
new file mode 100644
index 0000000..ca20f73
--- /dev/null
+++ b/Tethering/res/drawable-ldpi/stat_sys_tether_general.png
Binary files differ
diff --git a/Tethering/res/drawable-ldpi/stat_sys_tether_usb.png b/Tethering/res/drawable-ldpi/stat_sys_tether_usb.png
new file mode 100644
index 0000000..65e9075
--- /dev/null
+++ b/Tethering/res/drawable-ldpi/stat_sys_tether_usb.png
Binary files differ
diff --git a/Tethering/res/drawable-mdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-mdpi/stat_sys_tether_bluetooth.png
new file mode 100644
index 0000000..f42dae0
--- /dev/null
+++ b/Tethering/res/drawable-mdpi/stat_sys_tether_bluetooth.png
Binary files differ
diff --git a/Tethering/res/drawable-mdpi/stat_sys_tether_general.png b/Tethering/res/drawable-mdpi/stat_sys_tether_general.png
new file mode 100644
index 0000000..0655161
--- /dev/null
+++ b/Tethering/res/drawable-mdpi/stat_sys_tether_general.png
Binary files differ
diff --git a/Tethering/res/drawable-mdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-mdpi/stat_sys_tether_usb.png
new file mode 100644
index 0000000..2e2b8ca
--- /dev/null
+++ b/Tethering/res/drawable-mdpi/stat_sys_tether_usb.png
Binary files differ
diff --git a/Tethering/res/drawable-xhdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-xhdpi/stat_sys_tether_bluetooth.png
new file mode 100644
index 0000000..3f57d1c
--- /dev/null
+++ b/Tethering/res/drawable-xhdpi/stat_sys_tether_bluetooth.png
Binary files differ
diff --git a/Tethering/res/drawable-xhdpi/stat_sys_tether_general.png b/Tethering/res/drawable-xhdpi/stat_sys_tether_general.png
new file mode 100644
index 0000000..34b0cb3
--- /dev/null
+++ b/Tethering/res/drawable-xhdpi/stat_sys_tether_general.png
Binary files differ
diff --git a/Tethering/res/drawable-xhdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-xhdpi/stat_sys_tether_usb.png
new file mode 100644
index 0000000..36afe48
--- /dev/null
+++ b/Tethering/res/drawable-xhdpi/stat_sys_tether_usb.png
Binary files differ
diff --git a/Tethering/res/drawable-xxhdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-xxhdpi/stat_sys_tether_bluetooth.png
new file mode 100644
index 0000000..25acfbb
--- /dev/null
+++ b/Tethering/res/drawable-xxhdpi/stat_sys_tether_bluetooth.png
Binary files differ
diff --git a/Tethering/res/drawable-xxhdpi/stat_sys_tether_general.png b/Tethering/res/drawable-xxhdpi/stat_sys_tether_general.png
new file mode 100644
index 0000000..5c65601
--- /dev/null
+++ b/Tethering/res/drawable-xxhdpi/stat_sys_tether_general.png
Binary files differ
diff --git a/Tethering/res/drawable-xxhdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-xxhdpi/stat_sys_tether_usb.png
new file mode 100644
index 0000000..28b4b54
--- /dev/null
+++ b/Tethering/res/drawable-xxhdpi/stat_sys_tether_usb.png
Binary files differ
diff --git a/Tethering/res/values-af/strings.xml b/Tethering/res/values-af/strings.xml
new file mode 100644
index 0000000..056168b
--- /dev/null
+++ b/Tethering/res/values-af/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Verbinding of warmkol is aktief"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tik om op te stel."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Verbinding is gedeaktiveer"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontak jou administrateur vir besonderhede"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Warmkol- en verbindingstatus"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-am/strings.xml b/Tethering/res/values-am/strings.xml
new file mode 100644
index 0000000..ac468dd
--- /dev/null
+++ b/Tethering/res/values-am/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"እንደ ሞደም መሰካት ወይም መገናኛ ነጥብ ገባሪ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ለማዋቀር መታ ያድርጉ።"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"እንደ ሞደም መሰካት ተሰናክሏል"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ለዝርዝሮች የእርስዎን አስተዳዳሪ ያነጋግሩ"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"መገናኛ ነጥብ እና እንደ ሞደም የመሰካት ሁኔታ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ar/strings.xml b/Tethering/res/values-ar/strings.xml
new file mode 100644
index 0000000..7d5bad3
--- /dev/null
+++ b/Tethering/res/values-ar/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"النطاق نشط أو نقطة الاتصال نشطة"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"انقر للإعداد."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"التوصيل متوقف."</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"تواصَل مع المشرف للحصول على التفاصيل."</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"حالة نقطة الاتصال والتوصيل"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-as/strings.xml b/Tethering/res/values-as/strings.xml
new file mode 100644
index 0000000..0913504
--- /dev/null
+++ b/Tethering/res/values-as/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"টে\'ডাৰিং অথবা হ\'টস্প\'ট সক্ৰিয় অৱস্থাত আছে"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ছেট আপ কৰিবলৈ টিপক।"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"টে\'ডাৰিঙৰ সুবিধাটো অক্ষম কৰি থোৱা হৈছে"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"সবিশেষ জানিবলৈ আপোনাৰ প্ৰশাসকৰ সৈতে যোগাযোগ কৰক"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"হ’টস্প\'ট আৰু টে\'ডাৰিঙৰ স্থিতি"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-az/strings.xml b/Tethering/res/values-az/strings.xml
new file mode 100644
index 0000000..dce70da
--- /dev/null
+++ b/Tethering/res/values-az/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Birləşmə və ya hotspot aktivdir"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Ayarlamaq üçün toxunun."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Birləşmə deaktivdir"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Detallar üçün adminlə əlaqə saxlayın"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot & birləşmə statusu"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-b+sr+Latn/strings.xml b/Tethering/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..b0774ec
--- /dev/null
+++ b/Tethering/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Privezivanje ili hotspot je aktivan"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Dodirnite da biste podesili."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Privezivanje je onemogućeno"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Potražite detalje od administratora"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status hotspota i privezivanja"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-be/strings.xml b/Tethering/res/values-be/strings.xml
new file mode 100644
index 0000000..a8acebe
--- /dev/null
+++ b/Tethering/res/values-be/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Мадэм або хот-спот актыўныя"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Дакраніцеся, каб наладзіць."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Рэжым мадэма выключаны"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Звярніцеся да адміністратара па падрабязную інфармацыю"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Стан \"Хот-спот і мадэм\""</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-bg/strings.xml b/Tethering/res/values-bg/strings.xml
new file mode 100644
index 0000000..94fb2d8
--- /dev/null
+++ b/Tethering/res/values-bg/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Има активна споделена връзка или точка за достъп"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Докоснете, за да настроите."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Функцията за тетъринг е деактивирана"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Свържете се с администратора си за подробности"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Състояние на функцията за точка за достъп и тетъринг"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-bn/strings.xml b/Tethering/res/values-bn/strings.xml
new file mode 100644
index 0000000..aea02b9
--- /dev/null
+++ b/Tethering/res/values-bn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"টিথারিং বা হটস্পট চালু আছে"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"সেট-আপ করতে ট্যাপ করুন।"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"টিথারিং বন্ধ করা আছে"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"বিশদে জানতে অ্যাডমিনের সাথে যোগাযোগ করুন"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"হটস্পট ও টিথারিং স্ট্যাটাস"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-bs/strings.xml b/Tethering/res/values-bs/strings.xml
new file mode 100644
index 0000000..de23272
--- /dev/null
+++ b/Tethering/res/values-bs/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Aktivno je povezivanje putem mobitela ili pristupna tačka"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Dodirnite da postavite."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Povezivanje putem mobitela je onemogućeno"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontaktirajte svog administratora za detalje"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status pristupne tačke i povezivanja putem mobitela"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ca/strings.xml b/Tethering/res/values-ca/strings.xml
new file mode 100644
index 0000000..88b795c
--- /dev/null
+++ b/Tethering/res/values-ca/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Compartició de xarxa o punt d\'accés Wi‑Fi actius"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Toca per configurar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"La compartició de xarxa està desactivada"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contacta amb el teu administrador per obtenir més informació"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estat del punt d\'accés Wi‑Fi i de la compartició de xarxa"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-cs/strings.xml b/Tethering/res/values-cs/strings.xml
new file mode 100644
index 0000000..8c1b83b
--- /dev/null
+++ b/Tethering/res/values-cs/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering nebo hotspot je aktivní"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Klepnutím zahájíte nastavení."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering je zakázán"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"O podrobnosti požádejte administrátora"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stav hotspotu a tetheringu"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-da/strings.xml b/Tethering/res/values-da/strings.xml
new file mode 100644
index 0000000..f413e70
--- /dev/null
+++ b/Tethering/res/values-da/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Netdeling eller hotspot er aktivt"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tryk for at konfigurere."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Netdeling er deaktiveret"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontakt din administrator for at få oplysninger"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status for hotspot og netdeling"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-de/strings.xml b/Tethering/res/values-de/strings.xml
new file mode 100644
index 0000000..f057d78
--- /dev/null
+++ b/Tethering/res/values-de/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering oder Hotspot aktiv"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Zum Einrichten tippen."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering ist deaktiviert"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Bitte wende dich für weitere Informationen an den Administrator"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot- und Tethering-Status"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-el/strings.xml b/Tethering/res/values-el/strings.xml
new file mode 100644
index 0000000..b3c986b
--- /dev/null
+++ b/Tethering/res/values-el/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Πρόσδεση ή σύνδεση σημείου πρόσβασης ενεργή"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Πατήστε για ρύθμιση."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Η σύνδεση είναι απενεργοποιημένη"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Επικοινωνήστε με τον διαχειριστή σας για λεπτομέρειες"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Κατάσταση σημείου πρόσβασης Wi-Fi και σύνδεσης"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-en-rAU/strings.xml b/Tethering/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..769e012
--- /dev/null
+++ b/Tethering/res/values-en-rAU/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-en-rCA/strings.xml b/Tethering/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..769e012
--- /dev/null
+++ b/Tethering/res/values-en-rCA/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-en-rGB/strings.xml b/Tethering/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..769e012
--- /dev/null
+++ b/Tethering/res/values-en-rGB/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-en-rIN/strings.xml b/Tethering/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..769e012
--- /dev/null
+++ b/Tethering/res/values-en-rIN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot and tethering status"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-en-rXC/strings.xml b/Tethering/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..f1674be
--- /dev/null
+++ b/Tethering/res/values-en-rXC/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering or hotspot active"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tap to set up."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is disabled"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contact your admin for details"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot & tethering status"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-es-rUS/strings.xml b/Tethering/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..63689f4
--- /dev/null
+++ b/Tethering/res/values-es-rUS/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Conexión a red o hotspot conectados"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Presiona para configurar esta opción."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Se inhabilitó la conexión mediante dispositivo portátil"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Para obtener más información, comunícate con el administrador"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado del hotspot y la conexión mediante dispositivo portátil"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-es/strings.xml b/Tethering/res/values-es/strings.xml
new file mode 100644
index 0000000..9a34ed5
--- /dev/null
+++ b/Tethering/res/values-es/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Conexión compartida o punto de acceso activos"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Toca para configurar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"La conexión compartida está inhabilitada"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Solicita más información a tu administrador"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado del punto de acceso y de la conexión compartida"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-et/strings.xml b/Tethering/res/values-et/strings.xml
new file mode 100644
index 0000000..0970341
--- /dev/null
+++ b/Tethering/res/values-et/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Jagamine või kuumkoht on aktiivne"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Puudutage seadistamiseks."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Jagamine on keelatud"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Lisateabe saamiseks võtke ühendust oma administraatoriga"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Kuumkoha ja jagamise olek"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-eu/strings.xml b/Tethering/res/values-eu/strings.xml
new file mode 100644
index 0000000..632019e
--- /dev/null
+++ b/Tethering/res/values-eu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Konexioa partekatzea edo wifi-gunea aktibo dago"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Sakatu konfiguratzeko."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Desgaituta dago konexioa partekatzeko aukera"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Xehetasunak lortzeko, jarri administratzailearekin harremanetan"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Wifi-gunearen eta konexioa partekatzeko eginbidearen egoera"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-fa/strings.xml b/Tethering/res/values-fa/strings.xml
new file mode 100644
index 0000000..2e21c85
--- /dev/null
+++ b/Tethering/res/values-fa/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"اشتراکگذاری اینترنت یا نقطه اتصال فعال"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"برای راهاندازی ضربه بزنید."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"اشتراکگذاری اینترنت غیرفعال است"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"برای جزئیات، با سرپرستتان تماس بگیرید"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"وضعیت نقطه اتصال و اشتراکگذاری اینترنت"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-fi/strings.xml b/Tethering/res/values-fi/strings.xml
new file mode 100644
index 0000000..413db3f
--- /dev/null
+++ b/Tethering/res/values-fi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Yhteyden jakaminen tai hotspot käytössä"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Ota käyttöön napauttamalla."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Yhteyden jakaminen on poistettu käytöstä"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Pyydä lisätietoja järjestelmänvalvojalta"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspotin ja yhteyden jakamisen tila"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-fr-rCA/strings.xml b/Tethering/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..eb2e4ba
--- /dev/null
+++ b/Tethering/res/values-fr-rCA/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Partage de connexion ou point d\'accès sans fil activé"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Touchez pour configurer."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Le partage de connexion est désactivé"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Communiquez avec votre administrateur pour obtenir plus de détails"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Point d\'accès et partage de connexion"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-fr/strings.xml b/Tethering/res/values-fr/strings.xml
new file mode 100644
index 0000000..22259c5
--- /dev/null
+++ b/Tethering/res/values-fr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Partage de connexion ou point d\'accès activé"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Appuyez pour effectuer la configuration."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Le partage de connexion est désactivé"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Pour en savoir plus, contactez votre administrateur"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"État du point d\'accès et du partage de connexion"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-gl/strings.xml b/Tethering/res/values-gl/strings.xml
new file mode 100644
index 0000000..ded82fc
--- /dev/null
+++ b/Tethering/res/values-gl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Conexión compartida ou zona wifi activada"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Toca para configurar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"A conexión compartida está desactivada"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contacta co administrador para obter información"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado da zona wifi e da conexión compartida"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-gu/strings.xml b/Tethering/res/values-gu/strings.xml
new file mode 100644
index 0000000..7cbbc2d
--- /dev/null
+++ b/Tethering/res/values-gu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ઇન્ટરનેટ શેર કરવાની સુવિધા અથવા હૉટસ્પૉટ સક્રિય છે"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"સેટઅપ કરવા માટે ટૅપ કરો."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરી છે"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"વિગતો માટે તમારા વ્યવસ્થાપકનો સંપર્ક કરો"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"હૉટસ્પૉટ અને ઇન્ટરનેટ શેર કરવાની સુવિધાનું સ્ટેટસ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-hi/strings.xml b/Tethering/res/values-hi/strings.xml
new file mode 100644
index 0000000..08af81b
--- /dev/null
+++ b/Tethering/res/values-hi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"टेदरिंग या हॉटस्पॉट चालू है"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"सेट अप करने के लिए टैप करें."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"टेदरिंग बंद है"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"जानकारी के लिए अपने एडमिन से संपर्क करें"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"हॉटस्पॉट और टेदरिंग की स्थिति"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-hr/strings.xml b/Tethering/res/values-hr/strings.xml
new file mode 100644
index 0000000..827c135
--- /dev/null
+++ b/Tethering/res/values-hr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Modemsko povezivanje ili žarišna točka aktivni"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Dodirnite da biste postavili."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Modemsko je povezivanje onemogućeno"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Obratite se administratoru da biste saznali pojedinosti"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status žarišne točke i modemskog povezivanja"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-hu/strings.xml b/Tethering/res/values-hu/strings.xml
new file mode 100644
index 0000000..eb68d6b
--- /dev/null
+++ b/Tethering/res/values-hu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Megosztás vagy aktív hotspot"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Koppintson a beállításhoz."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Az internetmegosztás le van tiltva"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"A részletekért forduljon rendszergazdájához"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot és internetmegosztás állapota"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-hy/strings.xml b/Tethering/res/values-hy/strings.xml
new file mode 100644
index 0000000..912941e
--- /dev/null
+++ b/Tethering/res/values-hy/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Մոդեմի ռեժիմը միացված է"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Հպեք՝ կարգավորելու համար։"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Մոդեմի ռեժիմն անջատված է"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Մանրամասների համար դիմեք ձեր ադմինիստրատորին"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Թեժ կետի և մոդեմի ռեժիմի կարգավիճակը"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-in/strings.xml b/Tethering/res/values-in/strings.xml
new file mode 100644
index 0000000..a4e175a
--- /dev/null
+++ b/Tethering/res/values-in/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering atau hotspot aktif"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Ketuk untuk menyiapkan."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering dinonaktifkan"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hubungi admin untuk mengetahui detailnya"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status hotspot & tethering"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-is/strings.xml b/Tethering/res/values-is/strings.xml
new file mode 100644
index 0000000..e9f6670
--- /dev/null
+++ b/Tethering/res/values-is/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Kveikt á tjóðrun eða aðgangsstað"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Ýttu til að setja upp."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Slökkt er á tjóðrun"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hafðu samband við kerfisstjórann til að fá upplýsingar"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Staða heits reits og tjóðrunar"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-it/strings.xml b/Tethering/res/values-it/strings.xml
new file mode 100644
index 0000000..ffb9196
--- /dev/null
+++ b/Tethering/res/values-it/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Hotspot o tethering attivo"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tocca per impostare."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering disattivato"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contatta il tuo amministratore per avere informazioni dettagliate"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stato hotspot e tethering"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-iw/strings.xml b/Tethering/res/values-iw/strings.xml
new file mode 100644
index 0000000..7adcb47
--- /dev/null
+++ b/Tethering/res/values-iw/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"נקודה לשיתוף אינטרנט או שיתוף אינטרנט בין מכשירים: בסטטוס פעיל"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"יש להקיש כדי להגדיר."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"שיתוף האינטרנט בין מכשירים מושבת"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"לפרטים, יש לפנות למנהל המערכת"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"סטטוס של נקודה לשיתוף אינטרנט ושיתוף אינטרנט בין מכשירים"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ja/strings.xml b/Tethering/res/values-ja/strings.xml
new file mode 100644
index 0000000..f68a730
--- /dev/null
+++ b/Tethering/res/values-ja/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"テザリングまたはアクセス ポイントが有効です"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"タップしてセットアップします。"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"テザリングは無効に設定されています"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"詳しくは、管理者にお問い合わせください"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"アクセス ポイントとテザリングのステータス"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ka/strings.xml b/Tethering/res/values-ka/strings.xml
new file mode 100644
index 0000000..7c22e82
--- /dev/null
+++ b/Tethering/res/values-ka/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ტეტერინგი ან უსადენო ქსელი აქტიურია"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"შეეხეთ დასაყენებლად."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ტეტერინგი გათიშულია"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"დამატებითი ინფორმაციისთვის დაუკავშირდით თქვენს ადმინისტრატორს"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"უსადენო ქსელის და ტეტერინგის სტატუსი"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-kk/strings.xml b/Tethering/res/values-kk/strings.xml
new file mode 100644
index 0000000..0857d06
--- /dev/null
+++ b/Tethering/res/values-kk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Тетеринг немесе хотспот қосулы"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Реттеу үшін түртіңіз."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Тетеринг өшірілді."</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Мәліметтерді әкімшіден алыңыз."</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Хотспот және тетеринг күйі"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-km/strings.xml b/Tethering/res/values-km/strings.xml
new file mode 100644
index 0000000..536e3d1
--- /dev/null
+++ b/Tethering/res/values-km/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ការភ្ជាប់ ឬហតស្ប៉តកំពុងដំណើរការ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ចុចដើម្បីរៀបចំ។"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ការភ្ជាប់ត្រូវបានបិទ"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ទាក់ទងអ្នកគ្រប់គ្រងរបស់អ្នក ដើម្បីទទួលបានព័ត៌មានលម្អិត"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ស្ថានភាពនៃការភ្ជាប់ និងហតស្ប៉ត"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-kn/strings.xml b/Tethering/res/values-kn/strings.xml
new file mode 100644
index 0000000..32f5492
--- /dev/null
+++ b/Tethering/res/values-kn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ಟೆಥರಿಂಗ್ ಅಥವಾ ಹಾಟ್ಸ್ಪಾಟ್ ಸಕ್ರಿಯವಾಗಿದೆ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ಸೆಟಪ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ಟೆಥರಿಂಗ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ವಿವರಗಳಿಗಾಗಿ ನಿಮ್ಮ ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ಹಾಟ್ಸ್ಪಾಟ್ ಮತ್ತು ಟೆಥರಿಂಗ್ ಸ್ಥಿತಿ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ko/strings.xml b/Tethering/res/values-ko/strings.xml
new file mode 100644
index 0000000..156b247
--- /dev/null
+++ b/Tethering/res/values-ko/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"테더링 또는 핫스팟 사용"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"설정하려면 탭하세요."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"테더링이 사용 중지됨"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"자세한 정보는 관리자에게 문의하세요."</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"핫스팟 및 테더링 상태"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ky/strings.xml b/Tethering/res/values-ky/strings.xml
new file mode 100644
index 0000000..18ee5fd
--- /dev/null
+++ b/Tethering/res/values-ky/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Модем режими күйүп турат"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Жөндөө үчүн таптап коюңуз."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Телефонду модем катары колдонууга болбойт"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Кеңири маалымат үчүн администраторуңузга кайрылыңыз"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Байланыш түйүнүнүн жана модем режиминин статусу"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-lo/strings.xml b/Tethering/res/values-lo/strings.xml
new file mode 100644
index 0000000..b127670
--- /dev/null
+++ b/Tethering/res/values-lo/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ເປີດການປ່ອຍສັນຍານ ຫຼື ຮັອດສະປອດແລ້ວ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ແຕະເພື່ອຕັ້ງຄ່າ."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ການປ່ອຍສັນຍານຖືກປິດໄວ້"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ຕິດຕໍ່ຜູ້ເບິ່ງແຍງລະບົບສຳລັບລາຍລະອຽດ"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ສະຖານະຮັອດສະປອດ ແລະ ການປ່ອຍສັນຍານ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-lt/strings.xml b/Tethering/res/values-lt/strings.xml
new file mode 100644
index 0000000..8427baf
--- /dev/null
+++ b/Tethering/res/values-lt/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Įrenginys naudojamas kaip modemas arba įjungtas viešosios interneto prieigos taškas"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Palieskite, kad nustatytumėte."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Įrenginio kaip modemo naudojimas išjungtas"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Jei reikia išsamios informacijos, susisiekite su administratoriumi"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Viešosios interneto prieigos taško ir įrenginio kaip modemo naudojimo būsena"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-lv/strings.xml b/Tethering/res/values-lv/strings.xml
new file mode 100644
index 0000000..aa2d699
--- /dev/null
+++ b/Tethering/res/values-lv/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Piesaiste vai tīklājs ir aktīvs."</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Pieskarieties, lai to iestatītu."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Piesaiste ir atspējota"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Lai iegūtu detalizētu informāciju, sazinieties ar savu administratoru."</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Tīklāja un piesaistes statuss"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-af/strings.xml b/Tethering/res/values-mcc204-mnc04-af/strings.xml
new file mode 100644
index 0000000..052ca09
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-af/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Warmkol het nie internet nie"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Toestelle kan nie aan internet koppel nie"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Skakel warmkol af"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Warmkol is aan"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Bykomende heffings kan geld terwyl jy swerf"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Gaan voort"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-am/strings.xml b/Tethering/res/values-mcc204-mnc04-am/strings.xml
new file mode 100644
index 0000000..0518c5a
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-am/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"መገናኛ ነጥቡ በይነመረብ የለውም"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"መሣሪያዎች ከበይነመረብ ጋር መገናኘት አይችሉም"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"መገናኛ ነጥብ ያጥፉ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"የመገናኛ ነጥብ በርቷል"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ቀጥል"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ar/strings.xml b/Tethering/res/values-mcc204-mnc04-ar/strings.xml
new file mode 100644
index 0000000..e6d8423
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ar/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"نقطة الاتصال غير متصلة بالإنترنت."</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"لا يمكن للأجهزة الاتصال بالإنترنت."</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"إيقاف نقطة الاتصال"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"نقطة الاتصال مفعّلة"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"قد يتم تطبيق رسوم إضافية أثناء التجوال."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"متابعة"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-as/strings.xml b/Tethering/res/values-mcc204-mnc04-as/strings.xml
new file mode 100644
index 0000000..4c57f21
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-as/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"হটস্পটৰ কোনো ইণ্টাৰনেট নাই"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ডিভাইচসমূহ ইণ্টাৰনেটৰ সৈতে সংযোগ কৰিব নোৱাৰি"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"হটস্পট অফ কৰক"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"হটস্পট অন হৈ আছে"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"অব্যাহত ৰাখক"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-az/strings.xml b/Tethering/res/values-mcc204-mnc04-az/strings.xml
new file mode 100644
index 0000000..2610ab1
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-az/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspotun internetə girişi yoxdur"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Cihazlar internetə qoşula bilmir"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Hotspot\'u deaktiv edin"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot aktivdir"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Davam edin"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-b+sr+Latn/strings.xml b/Tethering/res/values-mcc204-mnc04-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..7b032ba
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-b+sr+Latn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot nema pristup internetu"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Uređaji ne mogu da se povežu na internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Isključi hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot je uključen"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Možda važe dodatni troškovi u romingu"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Nastavi"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-be/strings.xml b/Tethering/res/values-mcc204-mnc04-be/strings.xml
new file mode 100644
index 0000000..2362a1e
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-be/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Хот-спот не падключаны да інтэрнэту"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Прылады не могуць падключацца да інтэрнэту"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Выключыць хот-спот"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Хот-спот уключаны"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Працягнуць"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-bg/strings.xml b/Tethering/res/values-mcc204-mnc04-bg/strings.xml
new file mode 100644
index 0000000..6ef1b0b
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-bg/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Точката за достъп няма връзка с интернет"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Устройствата не могат да се свържат с интернет"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Изключване на точката за достъп"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Точката за достъп е включена"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Напред"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-bn/strings.xml b/Tethering/res/values-mcc204-mnc04-bn/strings.xml
new file mode 100644
index 0000000..9a3033c
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-bn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"হটস্পটের সাথে ইন্টারনেট কানেক্ট করা নেই"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ডিভাইস ইন্টারনেটের সাথে কানেক্ট করতে পারছে না"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"হটস্পট বন্ধ করুন"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"হটস্পট চালু আছে"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"চালিয়ে যান"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-bs/strings.xml b/Tethering/res/values-mcc204-mnc04-bs/strings.xml
new file mode 100644
index 0000000..57f6d88
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-bs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Pristupna tačka nema internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Uređaji se ne mogu povezati na internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Isključi pristupnu tačku"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Pristupna tačka je uključena"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Mogu nastati dodatni troškovi u romingu"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Nastavi"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ca/strings.xml b/Tethering/res/values-mcc204-mnc04-ca/strings.xml
new file mode 100644
index 0000000..e3ad666
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ca/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"El punt d\'accés Wi‑Fi no té accés a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Els dispositius no es poden connectar a Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desactiva el punt d\'accés Wi‑Fi"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"El punt d\'accés Wi‑Fi està activat"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"És possible que s\'apliquin costos addicionals en itinerància"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continua"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-cs/strings.xml b/Tethering/res/values-mcc204-mnc04-cs/strings.xml
new file mode 100644
index 0000000..f099281
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-cs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot nemá připojení k internetu"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Zařízení se nemohou připojit k internetu"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Vypnout hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot je aktivní"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Pokračovat"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-da/strings.xml b/Tethering/res/values-mcc204-mnc04-da/strings.xml
new file mode 100644
index 0000000..1fb2374
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-da/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspottet har intet internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Enheder kan ikke oprette forbindelse til internettet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Deaktiver hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspottet er aktiveret"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Fortsæt"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-de/strings.xml b/Tethering/res/values-mcc204-mnc04-de/strings.xml
new file mode 100644
index 0000000..56d1d1d
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-de/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot ist nicht mit dem Internet verbunden"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Geräte können nicht mit dem Internet verbunden werden"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Hotspot deaktivieren"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot aktiviert"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Weiter"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-el/strings.xml b/Tethering/res/values-mcc204-mnc04-el/strings.xml
new file mode 100644
index 0000000..674f1f6
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-el/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Το σημείο πρόσβασης Wi-Fi δεν έχει πρόσβαση στο διαδίκτυο."</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Δεν είναι η δυνατή η σύνδεση των συσκευών στο διαδίκτυο."</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Απενεργοποίηση σημείου πρόσβασης Wi-Fi"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Σημείο πρόσβασης Wi-Fi ενεργό"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Συνέχεια"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-en-rAU/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rAU/strings.xml
new file mode 100644
index 0000000..3046a37
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-en-rAU/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Devices can’t connect to Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Turn off hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Additional charges may apply while roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continue"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-en-rCA/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rCA/strings.xml
new file mode 100644
index 0000000..3046a37
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-en-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Devices can’t connect to Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Turn off hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Additional charges may apply while roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continue"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-en-rGB/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rGB/strings.xml
new file mode 100644
index 0000000..3046a37
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-en-rGB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Devices can’t connect to Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Turn off hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Additional charges may apply while roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continue"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-en-rIN/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rIN/strings.xml
new file mode 100644
index 0000000..3046a37
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-en-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Devices can’t connect to Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Turn off hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Additional charges may apply while roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continue"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-en-rXC/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rXC/strings.xml
new file mode 100644
index 0000000..20c9b94
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-en-rXC/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot has no internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Devices can’t connect to internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Turn off hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Additional charges may apply while roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continue"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-es-rUS/strings.xml b/Tethering/res/values-mcc204-mnc04-es-rUS/strings.xml
new file mode 100644
index 0000000..956547c
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-es-rUS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"El hotspot no tiene conexión a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Los dispositivos no pueden conectarse a Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desactiva el hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"El hotspot está activado"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Es posible que se apliquen cargos adicionales por roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuar"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-es/strings.xml b/Tethering/res/values-mcc204-mnc04-es/strings.xml
new file mode 100644
index 0000000..831ec1f
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-es/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"El punto de acceso no tiene conexión a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Los dispositivos no se pueden conectar a Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desactivar punto de acceso"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Punto de acceso activado"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Puede que se apliquen cargos adicionales en itinerancia"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuar"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-et/strings.xml b/Tethering/res/values-mcc204-mnc04-et/strings.xml
new file mode 100644
index 0000000..ff8dde5
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-et/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Kuumkohal puudub Interneti-ühendus"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Seadmed ei saa Internetiga ühendust luua"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Lülita kuumkoht välja"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Kuumkoht on sees"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Jätka"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-eu/strings.xml b/Tethering/res/values-mcc204-mnc04-eu/strings.xml
new file mode 100644
index 0000000..c4f70a3
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-eu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Sare publikoak ez du Interneteko konexiorik"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Gailuak ezin dira konektatu Internetera"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desaktibatu sare publikoa"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Sare publikoa aktibatuta dago"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Egin aurrera"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-fa/strings.xml b/Tethering/res/values-mcc204-mnc04-fa/strings.xml
new file mode 100644
index 0000000..79e3ef1
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-fa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"نقطه اتصال به اینترنت دسترسی ندارد"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"دستگاهها به اینترنت متصل نشدند"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"نقطه اتصال را خاموش کنید"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"نقطه اتصال روشن است"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ممکن است درحین فراگردی تغییرات دیگر اعمال شود"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ادامه"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-fi/strings.xml b/Tethering/res/values-mcc204-mnc04-fi/strings.xml
new file mode 100644
index 0000000..64921bc
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-fi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspotilla ei ole internetyhteyttä"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Laitteet eivät voi yhdistää internetiin"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Laita hotspot pois päältä"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot on päällä"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Roaming voi aiheuttaa lisämaksuja"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Jatka"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-fr-rCA/strings.xml b/Tethering/res/values-mcc204-mnc04-fr-rCA/strings.xml
new file mode 100644
index 0000000..eda7b59
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-fr-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Le point d\'accès n\'est pas connecté à Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Appareils non connectés à Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Désactiver le point d\'accès"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Le point d\'accès est activé"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuer"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-fr/strings.xml b/Tethering/res/values-mcc204-mnc04-fr/strings.xml
new file mode 100644
index 0000000..eda7b59
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-fr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Le point d\'accès n\'est pas connecté à Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Appareils non connectés à Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Désactiver le point d\'accès"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Le point d\'accès est activé"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuer"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-gl/strings.xml b/Tethering/res/values-mcc204-mnc04-gl/strings.xml
new file mode 100644
index 0000000..c163c61
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-gl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"A zona wifi non ten acceso a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Os dispositivos non se poden conectar a Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desactivar zona wifi"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"A zona wifi está activada"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Pódense aplicar cargos adicionais en itinerancia"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuar"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-gu/strings.xml b/Tethering/res/values-mcc204-mnc04-gu/strings.xml
new file mode 100644
index 0000000..796d42e
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-gu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"હૉટસ્પૉટથી ઇન્ટરનેટ ચાલી રહ્યું નથી"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ડિવાઇસ, ઇન્ટરનેટ સાથે કનેક્ટ થઈ શકતા નથી"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"હૉટસ્પૉટ બંધ કરો"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"હૉટસ્પૉટ ચાલુ છે"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"આગળ વધો"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-hi/strings.xml b/Tethering/res/values-mcc204-mnc04-hi/strings.xml
new file mode 100644
index 0000000..a244200
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-hi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"हॉटस्पॉट से इंटरनेट नहीं चल रहा"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"डिवाइस इंटरनेट से कनेक्ट नहीं हो पा रहे"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"हॉटस्पॉट बंद करें"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"हॉटस्पॉट चालू है"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"जारी रखें"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-hr/strings.xml b/Tethering/res/values-mcc204-mnc04-hr/strings.xml
new file mode 100644
index 0000000..41618af
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-hr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Žarišna točka nema pristup internetu"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Uređaji se ne mogu povezati s internetom"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Isključi žarišnu točku"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Žarišna je točka uključena"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"U roamingu su mogući dodatni troškovi"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Nastavi"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-hu/strings.xml b/Tethering/res/values-mcc204-mnc04-hu/strings.xml
new file mode 100644
index 0000000..39b7a69
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-hu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"A hotspot nem csatlakozik az internethez"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Az eszközök nem tudnak csatlakozni az internethez"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Hotspot kikapcsolása"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"A hotspot be van kapcsolva"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Roaming során további díjak léphetnek fel"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Tovább"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-hy/strings.xml b/Tethering/res/values-mcc204-mnc04-hy/strings.xml
new file mode 100644
index 0000000..c14ae10
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-hy/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Թեժ կետը միացված չէ ինտերնետին"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Սարքերը չեն կարողանում միանալ ինտերնետին"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Անջատել թեժ կետը"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Թեժ կետը միացված է"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Շարունակել"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-in/strings.xml b/Tethering/res/values-mcc204-mnc04-in/strings.xml
new file mode 100644
index 0000000..1243d22
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-in/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot tidak memiliki koneksi internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Perangkat tidak dapat tersambung ke internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Nonaktifkan hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot aktif"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Biaya tambahan mungkin berlaku saat roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Lanjutkan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-is/strings.xml b/Tethering/res/values-mcc204-mnc04-is/strings.xml
new file mode 100644
index 0000000..82a7d01
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-is/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Heitur reitur er ekki nettengdur"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Tæki geta ekki tengst við internetið"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Slökkva á heitum reit"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Kveikt er á heitum reit"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Viðbótargjöld kunna að eiga við í reiki"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Halda áfram"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-it/strings.xml b/Tethering/res/values-mcc204-mnc04-it/strings.xml
new file mode 100644
index 0000000..a0f52dc
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-it/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"L\'hotspot non ha accesso a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"I dispositivi non possono connettersi a Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Disattiva l\'hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot attivo"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continua"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-iw/strings.xml b/Tethering/res/values-mcc204-mnc04-iw/strings.xml
new file mode 100644
index 0000000..80807bc
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-iw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"לנקודה לשיתוף אינטרנט אין חיבור לאינטרנט"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"המכשירים לא יכולים להתחבר לאינטרנט"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"כיבוי הנקודה לשיתוף אינטרנט"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"הנקודה לשיתוף אינטרנט פועלת"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ייתכנו חיובים נוספים בעת נדידה"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"המשך"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ja/strings.xml b/Tethering/res/values-mcc204-mnc04-ja/strings.xml
new file mode 100644
index 0000000..0e21a7f
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ja/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"アクセス ポイントがインターネットに接続されていません"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"デバイスをインターネットに接続できません"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"アクセス ポイントを OFF にする"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"アクセス ポイント: ON"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ローミング時に追加料金が発生することがあります"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"続行"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ka/strings.xml b/Tethering/res/values-mcc204-mnc04-ka/strings.xml
new file mode 100644
index 0000000..6d3b548
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ka/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"უსადენო ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"მოწყობილობები ვერ უკავშირდება ინტერნეტს"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"გამორთეთ უსადენო ქსელი"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"უსადენო ქსელი ჩართულია"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"გაგრძელება"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-kk/strings.xml b/Tethering/res/values-mcc204-mnc04-kk/strings.xml
new file mode 100644
index 0000000..985fc3f
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-kk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Хотспотта интернет жоқ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Құрылғылар интернетке қосылмайды"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Хотспотты өшіру"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Хотспот қосулы"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Жалғастыру"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-km/strings.xml b/Tethering/res/values-mcc204-mnc04-km/strings.xml
new file mode 100644
index 0000000..03b5cb6
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-km/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ហតស្ប៉តមិនមានអ៊ីនធឺណិតទេ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ឧបករណ៍មិនអាចភ្ជាប់អ៊ីនធឺណិតបានទេ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"បិទហតស្ប៉ត"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ហតស្ប៉តត្រូវបានបើក"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"អាចមានការគិតថ្លៃបន្ថែម នៅពេលរ៉ូមីង"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"បន្ត"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-kn/strings.xml b/Tethering/res/values-mcc204-mnc04-kn/strings.xml
new file mode 100644
index 0000000..f0adad8
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-kn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ಹಾಟ್ಸ್ಪಾಟ್ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ಇಂಟರ್ನೆಟ್ಗೆ ಸಂಪರ್ಕಗೊಳ್ಳಲು ಸಾಧನಗಳಿಗೆ ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ಹಾಟ್ಸ್ಪಾಟ್ ಆಫ್ ಮಾಡಿ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ಹಾಟ್ಸ್ಪಾಟ್ ಆನ್ ಆಗಿದೆ"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ರೋಮಿಂಗ್ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ಮುಂದುವರಿಸಿ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ko/strings.xml b/Tethering/res/values-mcc204-mnc04-ko/strings.xml
new file mode 100644
index 0000000..9218e9a
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ko/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"핫스팟이 인터넷에 연결되지 않음"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"기기를 인터넷에 연결할 수 없음"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"핫스팟 사용 중지"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"핫스팟 사용 중"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"로밍 중에는 추가 요금이 발생할 수 있습니다."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"계속"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ky/strings.xml b/Tethering/res/values-mcc204-mnc04-ky/strings.xml
new file mode 100644
index 0000000..35a060a
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ky/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Байланыш түйүнүндө Интернет жок"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Түзмөктөр Интернетке туташпай жатат"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Туташуу түйүнүн өчүрүү"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Кошулуу түйүнү күйүк"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Роумингде кошумча акы алынышы мүмкүн"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Улантуу"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-lo/strings.xml b/Tethering/res/values-mcc204-mnc04-lo/strings.xml
new file mode 100644
index 0000000..1d9203b
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-lo/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ຮັອດສະປອດບໍ່ມີອິນເຕີເນັດ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ອິນເຕີເນັດໄດ້"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ປິດຮັອດສະປອດ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ຮັອດສະປອດເປີດຢູ່"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ສືບຕໍ່"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-lt/strings.xml b/Tethering/res/values-mcc204-mnc04-lt/strings.xml
new file mode 100644
index 0000000..db5178b
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-lt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Nėra viešosios interneto prieigos taško interneto ryšio"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Įrenginiams nepavyksta prisijungti prie interneto"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Išjungti viešosios interneto prieigos tašką"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Viešosios interneto prieigos taškas įjungtas"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Tęsti"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-lv/strings.xml b/Tethering/res/values-mcc204-mnc04-lv/strings.xml
new file mode 100644
index 0000000..c712173
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-lv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Tīklājam nav interneta savienojuma"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Ierīces nevar izveidot savienojumu ar internetu"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Izslēgt tīklāju"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Tīklājs ir ieslēgts"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Viesabonēšanas laikā var tikt piemērota papildu samaksa"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Tālāk"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-mk/strings.xml b/Tethering/res/values-mcc204-mnc04-mk/strings.xml
new file mode 100644
index 0000000..aa44909
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-mk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Точката на пристап нема интернет"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Уредите не може да се поврзат на интернет"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Исклучи ја точката на пристап"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Точката на пристап е вклучена"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"При роаминг може да се наплатат дополнителни трошоци"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Продолжи"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ml/strings.xml b/Tethering/res/values-mcc204-mnc04-ml/strings.xml
new file mode 100644
index 0000000..d376fe5
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ml/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ഹോട്ട്സ്പോട്ടിൽ ഇന്റർനെറ്റ് ലഭ്യമല്ല"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ഉപകരണങ്ങൾ ഇന്റർനെറ്റിലേക്ക് കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ഹോട്ട്സ്പോട്ട് ഓഫാക്കുക"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ഹോട്ട്സ്പോട്ട് ഓണാണ്"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"തുടരുക"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-mn/strings.xml b/Tethering/res/values-mcc204-mnc04-mn/strings.xml
new file mode 100644
index 0000000..417213f
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-mn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Сүлжээний цэг дээр интернэт алга байна"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Төхөөрөмжүүд нь интернэтэд холбогдох боломжгүй байна"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Сүлжээний цэгийг унтраах"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Сүлжээний цэг асаалттай байна"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Үргэлжлүүлэх"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-mr/strings.xml b/Tethering/res/values-mcc204-mnc04-mr/strings.xml
new file mode 100644
index 0000000..2ed153fb1
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-mr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"हॉटस्पॉटला इंटरनेट नाही"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"डिव्हाइस इंटरनेटला कनेक्ट करू शकत नाहीत"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"हॉटस्पॉट बंद करा"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"हॉटस्पॉट सुरू आहे"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"सुरू ठेवा"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ms/strings.xml b/Tethering/res/values-mcc204-mnc04-ms/strings.xml
new file mode 100644
index 0000000..50817fd
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ms/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Tempat liputan tiada Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Peranti tidak dapat menyambung kepada Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Matikan tempat liputan"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Tempat liputan dihidupkan"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Caj tambahan mungkin digunakan semasa perayauan"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Teruskan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-my/strings.xml b/Tethering/res/values-mcc204-mnc04-my/strings.xml
new file mode 100644
index 0000000..c0d70e3
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-my/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ဟော့စပေါ့တွင် အင်တာနက်မရှိပါ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"စက်များက အင်တာနက်ချိတ်ဆက်၍ မရပါ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ဟော့စပေါ့ ပိတ်ရန်"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ဟော့စပေါ့ ဖွင့်ထားသည်"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ရှေ့ဆက်ရန်"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-nb/strings.xml b/Tethering/res/values-mcc204-mnc04-nb/strings.xml
new file mode 100644
index 0000000..1e7f1c6
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-nb/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Wi-Fi-sonen har ikke internettilgang"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Enheter kan ikke koble til internett"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Slå av Wi-Fi-sonen"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Wi-Fi-sonen er på"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Ytterligere kostnader kan påløpe under roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Fortsett"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ne/strings.xml b/Tethering/res/values-mcc204-mnc04-ne/strings.xml
new file mode 100644
index 0000000..63ce155
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ne/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"हटस्पटमा इन्टरनेट छैन"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"यन्त्रहरू इन्टरनेटमा कनेक्ट गर्न सकिएन"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"हटस्पट निष्क्रिय पार्नुहोस्"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"हटस्पट सक्रिय छ"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"जारी राख्नुहोस्"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-nl/strings.xml b/Tethering/res/values-mcc204-mnc04-nl/strings.xml
new file mode 100644
index 0000000..bf14a0f
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-nl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot heeft geen internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Apparaten kunnen geen verbinding maken met internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Hotspot uitschakelen"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot is ingeschakeld"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Er kunnen extra kosten voor roaming in rekening worden gebracht."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Doorgaan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-or/strings.xml b/Tethering/res/values-mcc204-mnc04-or/strings.xml
new file mode 100644
index 0000000..ab87b76
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-or/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ହଟସ୍ପଟରେ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ଡିଭାଇସଗୁଡ଼ିକ ଇଣ୍ଟର୍ନେଟ୍ ସହ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ହଟସ୍ପଟ ବନ୍ଦ କରନ୍ତୁ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ହଟସ୍ପଟ ଚାଲୁ ଅଛି"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ଜାରି ରଖନ୍ତୁ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-pa/strings.xml b/Tethering/res/values-mcc204-mnc04-pa/strings.xml
new file mode 100644
index 0000000..b09f285
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-pa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ਹੌਟਸਪੌਟ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"ਡੀਵਾਈਸ ਇੰਟਰਨੈੱਟ ਨਾਲ ਕਨੈਕਟ ਨਹੀਂ ਹੋ ਸਕਦੇ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ਹੌਟਸਪੌਟ ਬੰਦ ਕਰੋ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ਹੌਟਸਪੌਟ ਚਾਲੂ ਹੈ"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ਜਾਰੀ ਰੱਖੋ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-pl/strings.xml b/Tethering/res/values-mcc204-mnc04-pl/strings.xml
new file mode 100644
index 0000000..8becd07
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-pl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot nie ma internetu"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Urządzenia nie mogą połączyć się z internetem"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Wyłącz hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot jest włączony"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Dalej"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-pt-rBR/strings.xml b/Tethering/res/values-mcc204-mnc04-pt-rBR/strings.xml
new file mode 100644
index 0000000..8e01736
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-pt-rBR/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"O ponto de acesso não tem conexão com a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Não foi possível conectar os dispositivos à Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desativar ponto de acesso"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"O ponto de acesso está ativado"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Pode haver cobranças extras durante o roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuar"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-pt-rPT/strings.xml b/Tethering/res/values-mcc204-mnc04-pt-rPT/strings.xml
new file mode 100644
index 0000000..2356379
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-pt-rPT/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"A zona Wi-Fi não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Não é possível ligar os dispositivos à Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desativar zona Wi-Fi"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"A zona Wi-Fi está ativada"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Podem aplicar-se custos adicionais em roaming."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuar"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-pt/strings.xml b/Tethering/res/values-mcc204-mnc04-pt/strings.xml
new file mode 100644
index 0000000..8e01736
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-pt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"O ponto de acesso não tem conexão com a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Não foi possível conectar os dispositivos à Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Desativar ponto de acesso"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"O ponto de acesso está ativado"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Pode haver cobranças extras durante o roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuar"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ro/strings.xml b/Tethering/res/values-mcc204-mnc04-ro/strings.xml
new file mode 100644
index 0000000..2e62bd6
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ro/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspotul nu are internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Dispozitivele nu se pot conecta la internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Dezactivați hotspotul"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspotul este activ"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Se pot aplica taxe suplimentare pentru roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Continuați"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ru/strings.xml b/Tethering/res/values-mcc204-mnc04-ru/strings.xml
new file mode 100644
index 0000000..a2b1640
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ru/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Точка доступа не подключена к Интернету"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Устройства не могут подключаться к Интернету"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Отключить точку доступа"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Точка доступа включена"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Продолжить"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-si/strings.xml b/Tethering/res/values-mcc204-mnc04-si/strings.xml
new file mode 100644
index 0000000..632748a
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-si/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"හොට්ස්පොට් හට අන්තර්ජාලය නැත"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"උපාංගවලට අන්තර්ජාලයට සම්බන්ධ විය නොහැකිය"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"හොට්ස්පොට් ක්රියාවිරහිත කරන්න"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"හොට්ස්පොට් ක්රියාත්මකයි"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ඉදිරියට යන්න"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-sk/strings.xml b/Tethering/res/values-mcc204-mnc04-sk/strings.xml
new file mode 100644
index 0000000..247fc1b
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-sk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot nemá internetové pripojenie"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Zariadenia sa nedajú pripojiť k internetu"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Vypnúť hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot je zapnutý"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Pokračovať"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-sl/strings.xml b/Tethering/res/values-mcc204-mnc04-sl/strings.xml
new file mode 100644
index 0000000..ed22372
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-sl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Dostopna točka nima internetne povezave"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Naprave ne morejo vzpostaviti internetne povezave"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Izklopi dostopno točko"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Dostopna točka je vklopljena"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Med gostovanjem lahko nastanejo dodatni stroški"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Naprej"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-sq/strings.xml b/Tethering/res/values-mcc204-mnc04-sq/strings.xml
new file mode 100644
index 0000000..4bfab6e
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-sq/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Zona e qasjes për internet nuk ka internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Pajisjet nuk mund të lidhen me internetin"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Çaktivizo zonën e qasjes për internet"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Zona e qasjes për internet është aktive"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Vazhdo"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-sr/strings.xml b/Tethering/res/values-mcc204-mnc04-sr/strings.xml
new file mode 100644
index 0000000..478d53a
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-sr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Хотспот нема приступ интернету"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Уређаји не могу да се повежу на интернет"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Искључи хотспот"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Хотспот је укључен"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Можда важе додатни трошкови у ромингу"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Настави"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-sv/strings.xml b/Tethering/res/values-mcc204-mnc04-sv/strings.xml
new file mode 100644
index 0000000..a793ed6
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-sv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Surfzonen har ingen internetanslutning"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Enheterna har ingen internetanslutning"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Inaktivera surfzon"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Surfzonen är aktiverad"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Ytterligare avgifter kan tillkomma vid roaming"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Fortsätt"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-sw/strings.xml b/Tethering/res/values-mcc204-mnc04-sw/strings.xml
new file mode 100644
index 0000000..18ee457
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-sw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Mtandao pepe hauna intaneti"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Vifaa vimeshindwa kuunganisha kwenye intaneti"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Zima mtandao pepe"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Mtandao pepe umewashwa"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Endelea"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ta/strings.xml b/Tethering/res/values-mcc204-mnc04-ta/strings.xml
new file mode 100644
index 0000000..7eebd67
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ta/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ஹாட்ஸ்பாட்டில் இணையம் இல்லை"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"சாதனங்களால் இணையத்தில் இணைய இயலவில்லை"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ஹாட்ஸ்பாட்டை ஆஃப் செய்"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ஹாட்ஸ்பாட் ஆன் செய்யப்பட்டுள்ளது"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"தொடர்க"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-te/strings.xml b/Tethering/res/values-mcc204-mnc04-te/strings.xml
new file mode 100644
index 0000000..0986534
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-te/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"హాట్స్పాట్కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"పరికరాలను ఇంటర్నెట్కి కనెక్ట్ చేయడం సాధ్యం కాదు"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"హాట్స్పాట్ని ఆఫ్ చేయండి"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"హాట్స్పాట్ ఆన్లో ఉంది"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"రోమింగ్లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"కొనసాగించు"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-th/strings.xml b/Tethering/res/values-mcc204-mnc04-th/strings.xml
new file mode 100644
index 0000000..3837002
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-th/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ฮอตสปอตไม่ได้เชื่อมต่ออินเทอร์เน็ต"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"อุปกรณ์เชื่อมต่ออินเทอร์เน็ตไม่ได้"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ปิดฮอตสปอต"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ฮอตสปอตเปิดอยู่"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"ต่อไป"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-tl/strings.xml b/Tethering/res/values-mcc204-mnc04-tl/strings.xml
new file mode 100644
index 0000000..208f893
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-tl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Walang internet ang hotspot"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Hindi makakonekta sa internet ang mga device"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"I-off ang hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Naka-on ang hotspot"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Ituloy"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-tr/strings.xml b/Tethering/res/values-mcc204-mnc04-tr/strings.xml
new file mode 100644
index 0000000..3482faf
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-tr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot\'un internet bağlantısı yok"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Cihazlar internete bağlanamıyor"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Hotspot\'u kapat"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot açık"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Devam"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-uk/strings.xml b/Tethering/res/values-mcc204-mnc04-uk/strings.xml
new file mode 100644
index 0000000..dea3114
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-uk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Точка доступу не підключена до Інтернету"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Не вдається підключити пристрої до Інтернету"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Вимкнути точку доступу"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Точку доступу ввімкнено"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"У роумінгу може стягуватися додаткова плата"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Продовжити"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-ur/strings.xml b/Tethering/res/values-mcc204-mnc04-ur/strings.xml
new file mode 100644
index 0000000..09bc0c9
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-ur/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"ہاٹ اسپاٹ میں انٹرنیٹ نہیں ہے"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"آلات انٹرنیٹ سے منسلک نہیں ہو سکتے"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"ہاٹ اسپاٹ آف کریں"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"ہاٹ اسپاٹ آن ہے"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"جاری رکھیں"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-uz/strings.xml b/Tethering/res/values-mcc204-mnc04-uz/strings.xml
new file mode 100644
index 0000000..715d348
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-uz/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Hotspot internetga ulanmagan"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Qurilmalar internetga ulana olmayapti"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Hotspotni faolsizlantirish"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Hotspot yoniq"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Davom etish"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-vi/strings.xml b/Tethering/res/values-mcc204-mnc04-vi/strings.xml
new file mode 100644
index 0000000..bf4ee10
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-vi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"Điểm phát sóng không có kết nối Internet"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Các thiết bị không thể kết nối Internet"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Tắt điểm phát sóng"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"Điểm phát sóng đang bật"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Tiếp tục"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-zh-rCN/strings.xml b/Tethering/res/values-mcc204-mnc04-zh-rCN/strings.xml
new file mode 100644
index 0000000..cdb4224
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-zh-rCN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"热点没有网络连接"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"设备无法连接到互联网"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"关闭热点"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"热点已开启"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"漫游时可能会产生额外的费用"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"继续"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-zh-rHK/strings.xml b/Tethering/res/values-mcc204-mnc04-zh-rHK/strings.xml
new file mode 100644
index 0000000..3bb52e4
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-zh-rHK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"熱點沒有互聯網連線"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"裝置無法連線至互聯網"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"關閉熱點"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"已開啟熱點"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"漫遊時可能需要支付額外費用"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"繼續"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-zh-rTW/strings.xml b/Tethering/res/values-mcc204-mnc04-zh-rTW/strings.xml
new file mode 100644
index 0000000..298c3ea
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-zh-rTW/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"無線基地台沒有網際網路連線"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"裝置無法連上網際網路"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"關閉無線基地台"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"無線基地台已開啟"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"使用漫遊服務可能須支付額外費用"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"繼續"</string>
+</resources>
diff --git a/Tethering/res/values-mcc204-mnc04-zu/strings.xml b/Tethering/res/values-mcc204-mnc04-zu/strings.xml
new file mode 100644
index 0000000..3dc0078
--- /dev/null
+++ b/Tethering/res/values-mcc204-mnc04-zu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="6246167638178412020">"I-Hotspot ayina-inthanethi"</string>
+ <string name="no_upstream_notification_message" msgid="5010177541603431003">"Amadivayisi awakwazi ukuxhuma ku-inthanethi"</string>
+ <string name="no_upstream_notification_disable_button" msgid="2613861474440640595">"Vala i-hotspot"</string>
+ <string name="upstream_roaming_notification_title" msgid="3633925855626231152">"I-Hotspot ivuliwe"</string>
+ <string name="upstream_roaming_notification_message" msgid="1396837704184358258">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
+ <string name="upstream_roaming_notification_continue_button" msgid="5324117849715705638">"Qhubeka"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-af/strings.xml b/Tethering/res/values-mcc310-mnc004-af/strings.xml
new file mode 100644
index 0000000..19d659c
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-af/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Verbinding het nie internet nie"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Toestelle kan nie koppel nie"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Skakel verbinding af"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Warmkol of verbinding is aan"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Bykomende heffings kan geld terwyl jy swerf"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-am/strings.xml b/Tethering/res/values-mcc310-mnc004-am/strings.xml
new file mode 100644
index 0000000..8995430
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-am/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ማስተሳሰር ምንም በይነመረብ የለውም"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"መሣሪያዎችን ማገናኘት አይቻልም"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ማስተሳሰርን አጥፋ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"መገናኛ ነጥብ ወይም ማስተሳሰር በርቷል"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ar/strings.xml b/Tethering/res/values-mcc310-mnc004-ar/strings.xml
new file mode 100644
index 0000000..54f3b53
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ar/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ما مِن اتصال بالإنترنت خلال التوصيل"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"تعذّر اتصال الأجهزة"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"إيقاف التوصيل"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"نقطة الاتصال أو التوصيل مفعّلان"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"قد يتم تطبيق رسوم إضافية أثناء التجوال."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-as/strings.xml b/Tethering/res/values-mcc310-mnc004-as/strings.xml
new file mode 100644
index 0000000..e215141
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-as/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"টে\'ডাৰিঙৰ ইণ্টাৰনেট নাই"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"টে\'ডাৰিং অফ কৰক"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"হটস্পট অথবা টে\'ডাৰিং অন আছে"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-az/strings.xml b/Tethering/res/values-mcc310-mnc004-az/strings.xml
new file mode 100644
index 0000000..1fd8e4c
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-az/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Modemin internetə girişi yoxdur"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Cihazları qoşmaq mümkün deyil"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Modemi deaktiv edin"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot və ya modem aktivdir"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml b/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..1abe4f3
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Privezivanje nema pristup internetu"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Povezivanje uređaja nije uspelo"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Isključi privezivanje"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Uključen je hotspot ili privezivanje"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Možda važe dodatni troškovi u romingu"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-be/strings.xml b/Tethering/res/values-mcc310-mnc004-be/strings.xml
new file mode 100644
index 0000000..38dbd1e
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-be/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Не ўдалося падключыць прылады"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Выключыць рэжым мадэма"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Хот-спот або рэжым мадэма ўключаны"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-bg/strings.xml b/Tethering/res/values-mcc310-mnc004-bg/strings.xml
new file mode 100644
index 0000000..04b44db
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-bg/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Тетърингът няма връзка с интернет"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Устройствата не могат да установят връзка"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Изключване на тетъринга"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Точката за достъп или тетърингът са включени"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-bn/strings.xml b/Tethering/res/values-mcc310-mnc004-bn/strings.xml
new file mode 100644
index 0000000..579d1be
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-bn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"টিথারিং করার জন্য কোনও ইন্টারনেট কানেকশন নেই"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ডিভাইস কানেক্ট করতে পারছে না"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"টিথারিং বন্ধ করুন"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"হটস্পট বা টিথারিং চালু আছে"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-bs/strings.xml b/Tethering/res/values-mcc310-mnc004-bs/strings.xml
new file mode 100644
index 0000000..9ce3efe
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-bs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Povezivanje putem mobitela nema internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Uređaji se ne mogu povezati"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Isključi povezivanje putem mobitela"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Pristupna tačka ili povezivanje putem mobitela je uključeno"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Mogu nastati dodatni troškovi u romingu"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ca/strings.xml b/Tethering/res/values-mcc310-mnc004-ca/strings.xml
new file mode 100644
index 0000000..46d4c35
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ca/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"La compartició de xarxa no té accés a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"No es poden connectar els dispositius"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactiva la compartició de xarxa"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"S\'ha activat el punt d\'accés Wi‑Fi o la compartició de xarxa"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"És possible que s\'apliquin costos addicionals en itinerància"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-cs/strings.xml b/Tethering/res/values-mcc310-mnc004-cs/strings.xml
new file mode 100644
index 0000000..cc13860
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-cs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering nemá připojení k internetu"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Zařízení se nemůžou připojit"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Vypnout tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Je zapnutý hotspot nebo tethering"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-da/strings.xml b/Tethering/res/values-mcc310-mnc004-da/strings.xml
new file mode 100644
index 0000000..92c3ae1
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-da/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Netdeling har ingen internetforbindelse"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Enheder kan ikke oprette forbindelse"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Deaktiver netdeling"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot eller netdeling er aktiveret"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-de/strings.xml b/Tethering/res/values-mcc310-mnc004-de/strings.xml
new file mode 100644
index 0000000..967eb4d
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-de/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering hat keinen Internetzugriff"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Geräte können sich nicht verbinden"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tethering deaktivieren"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot oder Tethering ist aktiviert"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-el/strings.xml b/Tethering/res/values-mcc310-mnc004-el/strings.xml
new file mode 100644
index 0000000..5fb4974
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-el/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Δεν είναι δυνατή η σύνδεση των συσκευών"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Απενεργοποιήστε τη σύνδεση"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml
new file mode 100644
index 0000000..45647f9
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml
new file mode 100644
index 0000000..45647f9
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml
new file mode 100644
index 0000000..45647f9
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml
new file mode 100644
index 0000000..45647f9
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml
new file mode 100644
index 0000000..7877074
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering has no internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml b/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml
new file mode 100644
index 0000000..08edd81
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"La conexión mediante dispositivo móvil no tiene Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"No se pueden conectar los dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactivar conexión mediante dispositivo móvil"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Se activó el hotspot o la conexión mediante dispositivo móvil"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Es posible que se apliquen cargos adicionales por roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-es/strings.xml b/Tethering/res/values-mcc310-mnc004-es/strings.xml
new file mode 100644
index 0000000..79f51d0
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-es/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"La conexión no se puede compartir, porque no hay acceso a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Los dispositivos no se pueden conectar"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactivar conexión compartida"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Punto de acceso o conexión compartida activados"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Puede que se apliquen cargos adicionales en itinerancia"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-et/strings.xml b/Tethering/res/values-mcc310-mnc004-et/strings.xml
new file mode 100644
index 0000000..2da5f8a
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-et/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Jagamisel puudub internetiühendus"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Seadmed ei saa ühendust luua"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Lülita jagamine välja"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Kuumkoht või jagamine on sisse lülitatud"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
new file mode 100644
index 0000000..2073f28
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Ezin dira konektatu gailuak"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desaktibatu konexioa partekatzeko aukera"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fa/strings.xml b/Tethering/res/values-mcc310-mnc004-fa/strings.xml
new file mode 100644
index 0000000..e21b2a0
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-fa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"«اشتراکگذاری اینترنت» به اینترنت دسترسی ندارد"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"دستگاهها متصل نمیشوند"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"خاموش کردن «اشتراکگذاری اینترنت»"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"«نقطه اتصال» یا «اشتراکگذاری اینترنت» روشن است"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ممکن است درحین فراگردی تغییرات دیگر اعمال شود"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fi/strings.xml b/Tethering/res/values-mcc310-mnc004-fi/strings.xml
new file mode 100644
index 0000000..88b0b13
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-fi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Ei jaettavaa internetyhteyttä"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Laitteet eivät voi muodostaa yhteyttä"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Laita yhteyden jakaminen pois päältä"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot tai yhteyden jakaminen on päällä"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Roaming voi aiheuttaa lisämaksuja"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml b/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml
new file mode 100644
index 0000000..3b781bc
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Le partage de connexion n\'est pas connecté à Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Impossible de connecter les appareils"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Désactiver le partage de connexion"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Le point d\'accès ou le partage de connexion est activé"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-fr/strings.xml b/Tethering/res/values-mcc310-mnc004-fr/strings.xml
new file mode 100644
index 0000000..51d7203
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-fr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Aucune connexion à Internet n\'est disponible pour le partage de connexion"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Impossible de connecter les appareils"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Désactiver le partage de connexion"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Le point d\'accès ou le partage de connexion est activé"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-gl/strings.xml b/Tethering/res/values-mcc310-mnc004-gl/strings.xml
new file mode 100644
index 0000000..008ccb4
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-gl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"A conexión compartida non ten Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Non se puideron conectar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desactivar conexión compartida"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Está activada a zona wifi ou a conexión compartida"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Pódense aplicar cargos adicionais en itinerancia"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-gu/strings.xml b/Tethering/res/values-mcc310-mnc004-gu/strings.xml
new file mode 100644
index 0000000..f2e3b4d
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-gu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ડિવાઇસ કનેક્ટ કરી શકાતા નથી"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hi/strings.xml b/Tethering/res/values-mcc310-mnc004-hi/strings.xml
new file mode 100644
index 0000000..b11839d
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-hi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिंग से इंटरनेट नहीं चल रहा"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"डिवाइस कनेक्ट नहीं हो पा रहे"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिंग बंद करें"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हॉटस्पॉट या टेदरिंग चालू है"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hr/strings.xml b/Tethering/res/values-mcc310-mnc004-hr/strings.xml
new file mode 100644
index 0000000..0a5aca2
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-hr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Modemsko povezivanje nema internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Uređaji se ne mogu povezati"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Isključivanje modemskog povezivanja"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Uključena je žarišna točka ili modemsko povezivanje"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"U roamingu su mogući dodatni troškovi"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hu/strings.xml b/Tethering/res/values-mcc310-mnc004-hu/strings.xml
new file mode 100644
index 0000000..21c689a
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-hu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Nincs internetkapcsolat az internet megosztásához"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Az eszközök nem tudnak csatlakozni"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Internetmegosztás kikapcsolása"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"A hotspot vagy az internetmegosztás be van kapcsolva"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Roaming során további díjak léphetnek fel"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-hy/strings.xml b/Tethering/res/values-mcc310-mnc004-hy/strings.xml
new file mode 100644
index 0000000..689d928
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-hy/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Մոդեմի ռեժիմի կապը բացակայում է"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Չհաջողվեց միացնել սարքը"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Անջատել մոդեմի ռեժիմը"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Թեժ կետը կամ մոդեմի ռեժիմը միացված է"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-in/strings.xml b/Tethering/res/values-mcc310-mnc004-in/strings.xml
new file mode 100644
index 0000000..a5f4d19
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-in/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tidak ada koneksi internet di tethering"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Perangkat tidak dapat terhubung"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Nonaktifkan tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot atau tethering aktif"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Biaya tambahan mungkin berlaku saat roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-is/strings.xml b/Tethering/res/values-mcc310-mnc004-is/strings.xml
new file mode 100644
index 0000000..fc7e8aa
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-is/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tjóðrun er ekki með internettengingu"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Tæki geta ekki tengst"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Slökkva á tjóðrun"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Kveikt er á heitum reit eða tjóðrun"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Viðbótargjöld kunna að eiga við í reiki"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-it/strings.xml b/Tethering/res/values-mcc310-mnc004-it/strings.xml
new file mode 100644
index 0000000..6456dd1
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-it/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Nessuna connessione a Internet per il tethering"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Impossibile connettere i dispositivi"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Disattiva il tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot o tethering attivi"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-iw/strings.xml b/Tethering/res/values-mcc310-mnc004-iw/strings.xml
new file mode 100644
index 0000000..46b24bd
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-iw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"למכשירים אין אפשרות להתחבר"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"השבתה של שיתוף האינטרנט בין מכשירים"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ייתכנו חיובים נוספים בעת נדידה"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ja/strings.xml b/Tethering/res/values-mcc310-mnc004-ja/strings.xml
new file mode 100644
index 0000000..e6eb277
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ja/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"テザリングがインターネットに接続されていません"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"デバイスを接続できません"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"テザリングを OFF にする"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"アクセス ポイントまたはテザリングが ON です"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ローミング時に追加料金が発生することがあります"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ka/strings.xml b/Tethering/res/values-mcc310-mnc004-ka/strings.xml
new file mode 100644
index 0000000..aeddd71
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ka/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ტეტერინგს არ აქვს ინტერნეტზე წვდომა"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"მოწყობილობები ვერ ახერხებენ დაკავშირებას"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ტეტერინგის გამორთვა"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ჩართულია უსადენო ქსელი ან ტეტერინგი"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-kk/strings.xml b/Tethering/res/values-mcc310-mnc004-kk/strings.xml
new file mode 100644
index 0000000..255f0a2
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-kk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Тетеринг режимі интернет байланысынсыз пайдаланылуда"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Құрылғыларды байланыстыру мүмкін емес"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Тетерингіні өшіру"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Хотспот немесе тетеринг қосулы"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-km/strings.xml b/Tethering/res/values-mcc310-mnc004-km/strings.xml
new file mode 100644
index 0000000..2bceb1c
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-km/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ការភ្ជាប់មិនមានអ៊ីនធឺណិតទេ"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"មិនអាចភ្ជាប់ឧបករណ៍បានទេ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"បិទការភ្ជាប់"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ហតស្ប៉ត ឬការភ្ជាប់ត្រូវបានបើក"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"អាចមានការគិតថ្លៃបន្ថែម នៅពេលរ៉ូមីង"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-kn/strings.xml b/Tethering/res/values-mcc310-mnc004-kn/strings.xml
new file mode 100644
index 0000000..ed76930
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-kn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ಟೆಥರಿಂಗ್ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ಟೆಥರಿಂಗ್ ಆಫ್ ಮಾಡಿ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ಹಾಟ್ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್ ಆನ್ ಆಗಿದೆ"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ರೋಮಿಂಗ್ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ko/strings.xml b/Tethering/res/values-mcc310-mnc004-ko/strings.xml
new file mode 100644
index 0000000..6e50494
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ko/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"테더링으로 인터넷을 사용할 수 없음"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"기기에서 연결할 수 없음"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"테더링 사용 중지"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"핫스팟 또는 테더링 켜짐"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"로밍 중에는 추가 요금이 발생할 수 있습니다."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ky/strings.xml b/Tethering/res/values-mcc310-mnc004-ky/strings.xml
new file mode 100644
index 0000000..d68128b
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ky/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Модем режими Интернети жок колдонулууда"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Түзмөктөр туташпай жатат"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Модем режимин өчүрүү"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Байланыш түйүнү же модем режими күйүк"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Роумингде кошумча акы алынышы мүмкүн"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-lo/strings.xml b/Tethering/res/values-mcc310-mnc004-lo/strings.xml
new file mode 100644
index 0000000..03e134a
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-lo/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ປິດການປ່ອຍສັນຍານ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ເປີດໃຊ້ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານຢູ່"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-lt/strings.xml b/Tethering/res/values-mcc310-mnc004-lt/strings.xml
new file mode 100644
index 0000000..652cedc
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-lt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Nėra įrenginio kaip modemo naudojimo interneto ryšio"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Nepavyko susieti įrenginių"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Išjungti įrenginio kaip modemo naudojimą"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-lv/strings.xml b/Tethering/res/values-mcc310-mnc004-lv/strings.xml
new file mode 100644
index 0000000..2219722
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-lv/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Piesaistei nav interneta savienojuma"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Nevar savienot ierīces"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Izslēgt piesaisti"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ir ieslēgts tīklājs vai piesaiste"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Viesabonēšanas laikā var tikt piemērota papildu samaksa"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-mk/strings.xml b/Tethering/res/values-mcc310-mnc004-mk/strings.xml
new file mode 100644
index 0000000..227f9e3
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-mk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Нема интернет преку мобилен"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Уредите не може да се поврзат"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Исклучи интернет преку мобилен"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Точката на пристап или интернетот преку мобилен е вклучен"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"При роаминг може да се наплатат дополнителни трошоци"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ml/strings.xml b/Tethering/res/values-mcc310-mnc004-ml/strings.xml
new file mode 100644
index 0000000..ec43885
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ml/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ടെതറിംഗ് ഓഫാക്കുക"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ഹോട്ട്സ്പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-mn/strings.xml b/Tethering/res/values-mcc310-mnc004-mn/strings.xml
new file mode 100644
index 0000000..e263573
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-mn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Модемд интернэт алга байна"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Төхөөрөмжүүд холбогдох боломжгүй байна"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Модем болгохыг унтраах"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Сүлжээний цэг эсвэл модем болгох асаалттай байна"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-mr/strings.xml b/Tethering/res/values-mcc310-mnc004-mr/strings.xml
new file mode 100644
index 0000000..adf845d
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-mr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिंगला इंटरनेट नाही"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"डिव्हाइस कनेक्ट होऊ शकत नाहीत"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिंग बंद करा"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हॉटस्पॉट किंवा टेदरिंग सुरू आहे"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ms/strings.xml b/Tethering/res/values-mcc310-mnc004-ms/strings.xml
new file mode 100644
index 0000000..f65c451
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ms/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Penambatan tiada Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Peranti tidak dapat disambungkan"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Matikan penambatan"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Tempat liputan atau penambatan dihidupkan"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Caj tambahan mungkin digunakan semasa perayauan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-my/strings.xml b/Tethering/res/values-mcc310-mnc004-my/strings.xml
new file mode 100644
index 0000000..4118e77
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-my/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းတွင် အင်တာနက် မရှိပါ"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"စက်များ ချိတ်ဆက်၍ မရပါ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ပိတ်ရန်"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ဟော့စပေါ့ (သို့) မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ဖွင့်ထားသည်"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-nb/strings.xml b/Tethering/res/values-mcc310-mnc004-nb/strings.xml
new file mode 100644
index 0000000..3685358
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-nb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Internettdeling har ikke internettilgang"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Enhetene kan ikke koble til"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Slå av internettdeling"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Wi-Fi-sone eller internettdeling er på"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ytterligere kostnader kan påløpe under roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ne/strings.xml b/Tethering/res/values-mcc310-mnc004-ne/strings.xml
new file mode 100644
index 0000000..d074f15
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ne/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"यन्त्रहरू कनेक्ट गर्न सकिएन"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिङ निष्क्रिय पार्नुहोस्"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हटस्पट वा टेदरिङ सक्रिय छ"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-nl/strings.xml b/Tethering/res/values-mcc310-mnc004-nl/strings.xml
new file mode 100644
index 0000000..1d88894
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-nl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering heeft geen internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Apparaten kunnen niet worden verbonden"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tethering uitschakelen"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot of tethering is ingeschakeld"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Er kunnen extra kosten voor roaming in rekening worden gebracht."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-or/strings.xml b/Tethering/res/values-mcc310-mnc004-or/strings.xml
new file mode 100644
index 0000000..8038815
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-or/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ଡିଭାଇସଗୁଡ଼ିକ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ଟିଥରିଂ ବନ୍ଦ କରନ୍ତୁ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ହଟସ୍ପଟ୍ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pa/strings.xml b/Tethering/res/values-mcc310-mnc004-pa/strings.xml
new file mode 100644
index 0000000..819833e
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-pa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pl/strings.xml b/Tethering/res/values-mcc310-mnc004-pl/strings.xml
new file mode 100644
index 0000000..65e4380
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-pl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering nie ma internetu"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Urządzenia nie mogą się połączyć"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Wyłącz tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot lub tethering jest włączony"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml b/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml
new file mode 100644
index 0000000..d886617
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"O tethering não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Não é possível conectar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desativar o tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ponto de acesso ou tethering ativado"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Pode haver cobranças extras durante o roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml b/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml
new file mode 100644
index 0000000..bfd45ca
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"A ligação (à Internet) via telemóvel não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Não é possível ligar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desativar ligação (à Internet) via telemóvel"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Podem aplicar-se custos adicionais em roaming."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-pt/strings.xml b/Tethering/res/values-mcc310-mnc004-pt/strings.xml
new file mode 100644
index 0000000..d886617
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-pt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"O tethering não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Não é possível conectar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Desativar o tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Ponto de acesso ou tethering ativado"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Pode haver cobranças extras durante o roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ro/strings.xml b/Tethering/res/values-mcc310-mnc004-ro/strings.xml
new file mode 100644
index 0000000..8d87a9e
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ro/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Procesul de tethering nu are internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Dispozitivele nu se pot conecta"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Dezactivați procesul de tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"S-a activat hotspotul sau tethering"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Se pot aplica taxe suplimentare pentru roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ru/strings.xml b/Tethering/res/values-mcc310-mnc004-ru/strings.xml
new file mode 100644
index 0000000..dbdb9eb
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ru/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Режим модема используется без доступа к Интернету"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Невозможно подключить устройства."</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Отключить режим модема"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Включены точка доступа или режим модема"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-si/strings.xml b/Tethering/res/values-mcc310-mnc004-si/strings.xml
new file mode 100644
index 0000000..d8301e4
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-si/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ටෙදරින් හට අන්තර්ජාලය නැත"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"උපාංගවලට සම්බන්ධ විය නොහැකිය"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ටෙදරින් ක්රියාවිරහිත කරන්න"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"හොට්ස්පොට් හෝ ටෙදරින් ක්රියාත්මකයි"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sk/strings.xml b/Tethering/res/values-mcc310-mnc004-sk/strings.xml
new file mode 100644
index 0000000..bef7136
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-sk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering nemá internetové pripojenie"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Zariadenia sa nemôžu pripojiť"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Vypnúť tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Je zapnutý hotspot alebo tethering"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sl/strings.xml b/Tethering/res/values-mcc310-mnc004-sl/strings.xml
new file mode 100644
index 0000000..3202c62
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-sl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Internetna povezava prek mobilnega telefona ni vzpostavljena"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Napravi se ne moreta povezati"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Izklopi internetno povezavo prek mobilnega telefona"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Dostopna točka ali internetna povezava prek mobilnega telefona je vklopljena"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Med gostovanjem lahko nastanejo dodatni stroški"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sq/strings.xml b/Tethering/res/values-mcc310-mnc004-sq/strings.xml
new file mode 100644
index 0000000..37f6ad2
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-sq/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Ndarja e internetit nuk ka internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Pajisjet nuk mund të lidhen"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Çaktivizo ndarjen e internetit"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Zona e qasjes për internet ose ndarja e internetit është aktive"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sr/strings.xml b/Tethering/res/values-mcc310-mnc004-sr/strings.xml
new file mode 100644
index 0000000..5566d03
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-sr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Привезивање нема приступ интернету"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Повезивање уређаја није успело"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Искључи привезивање"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Укључен је хотспот или привезивање"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Можда важе додатни трошкови у ромингу"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sv/strings.xml b/Tethering/res/values-mcc310-mnc004-sv/strings.xml
new file mode 100644
index 0000000..9765acd
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-sv/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Det finns ingen internetanslutning för internetdelningen"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Enheterna kan inte anslutas"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Inaktivera internetdelning"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Surfzon eller internetdelning har aktiverats"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Ytterligare avgifter kan tillkomma vid roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-sw/strings.xml b/Tethering/res/values-mcc310-mnc004-sw/strings.xml
new file mode 100644
index 0000000..cf850c9
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-sw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Kipengele cha kusambaza mtandao hakina intaneti"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Imeshindwa kuunganisha vifaa"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Zima kipengele cha kusambaza mtandao"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ta/strings.xml b/Tethering/res/values-mcc310-mnc004-ta/strings.xml
new file mode 100644
index 0000000..f4b15aa
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ta/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"சாதனங்களால் இணைய முடியவில்லை"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"இணைப்பு முறையை ஆஃப் செய்"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-te/strings.xml b/Tethering/res/values-mcc310-mnc004-te/strings.xml
new file mode 100644
index 0000000..937d34d
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-te/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"పరికరాలు కనెక్ట్ అవ్వడం లేదు"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"టెథరింగ్ను ఆఫ్ చేయండి"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"హాట్స్పాట్ లేదా టెథరింగ్ ఆన్లో ఉంది"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"రోమింగ్లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-th/strings.xml b/Tethering/res/values-mcc310-mnc004-th/strings.xml
new file mode 100644
index 0000000..f781fae
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-th/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือไม่มีอินเทอร์เน็ต"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"อุปกรณ์เชื่อมต่อไม่ได้"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-tl/strings.xml b/Tethering/res/values-mcc310-mnc004-tl/strings.xml
new file mode 100644
index 0000000..8d5d465
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-tl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Walang internet ang pag-tether"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Hindi makakonekta ang mga device"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"I-off ang pag-tether"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Naka-on ang Hotspot o pag-tether"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-tr/strings.xml b/Tethering/res/values-mcc310-mnc004-tr/strings.xml
new file mode 100644
index 0000000..80cab33
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-tr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Tethering\'in internet bağlantısı yok"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Cihazlar bağlanamıyor"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tethering\'i kapat"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot veya tethering açık"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-uk/strings.xml b/Tethering/res/values-mcc310-mnc004-uk/strings.xml
new file mode 100644
index 0000000..c05932a
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-uk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Телефон, який використовується як модем, не підключений до Інтернету"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Не вдається підключити пристрої"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Вимкнути використання телефона як модема"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Увімкнено точку доступу або використання телефона як модема"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"У роумінгу може стягуватися додаткова плата"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ur/strings.xml b/Tethering/res/values-mcc310-mnc004-ur/strings.xml
new file mode 100644
index 0000000..d820eee
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-ur/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"ٹیدرنگ میں انٹرنیٹ نہیں ہے"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"آلات منسلک نہیں ہو سکتے"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"ٹیدرنگ آف کریں"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ہاٹ اسپاٹ یا ٹیدرنگ آن ہے"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-uz/strings.xml b/Tethering/res/values-mcc310-mnc004-uz/strings.xml
new file mode 100644
index 0000000..726148a
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-uz/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Modem internetga ulanmagan"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Qurilmalar ulanmadi"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Modem rejimini faolsizlantirish"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Hotspot yoki modem rejimi yoniq"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-vi/strings.xml b/Tethering/res/values-mcc310-mnc004-vi/strings.xml
new file mode 100644
index 0000000..b7cb045
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-vi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Không có Internet để chia sẻ kết Internet"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Các thiết bị không thể kết nối"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Tắt tính năng chia sẻ Internet"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml
new file mode 100644
index 0000000..af91aff
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"共享网络未连接到互联网"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"设备无法连接"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"关闭网络共享"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"热点或网络共享已开启"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"漫游时可能会产生额外的费用"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml
new file mode 100644
index 0000000..28e6b80
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"無法透過網絡共享連線至互聯網"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"裝置無法連接"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"關閉網絡共享"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"熱點或網絡共享已開啟"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"漫遊時可能需要支付額外費用"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
new file mode 100644
index 0000000..528a1e5
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"無法透過網路共用連上網際網路"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"裝置無法連線"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"關閉網路共用"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"無線基地台或網路共用已開啟"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"使用漫遊服務可能須支付額外費用"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zu/strings.xml b/Tethering/res/values-mcc310-mnc004-zu/strings.xml
new file mode 100644
index 0000000..11eb666
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004-zu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="5030042590486713460">"Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi"</string>
+ <string name="no_upstream_notification_message" msgid="3843613362272973447">"Amadivayisi awakwazi ukuxhumeka"</string>
+ <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"Vala ukusebenzisa ifoni njengemodemu"</string>
+ <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe"</string>
+ <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
+</resources>
diff --git a/Tethering/res/values-mcc310-mnc004/config.xml b/Tethering/res/values-mcc310-mnc004/config.xml
new file mode 100644
index 0000000..5c5be04
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
+ "0" for disable this feature. -->
+ <integer name="delay_to_show_no_upstream_after_no_backhaul">5000</integer>
+
+ <!-- Config for showing upstream roaming notification. -->
+ <bool name="config_upstream_roaming_notification">true</bool>
+</resources>
\ No newline at end of file
diff --git a/Tethering/res/values-mcc310-mnc004/strings.xml b/Tethering/res/values-mcc310-mnc004/strings.xml
new file mode 100644
index 0000000..ce9ff60
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- String for no upstream notification title [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_title">Tethering has no internet</string>
+ <!-- String for no upstream notification title [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_message">Devices can\u2019t connect</string>
+ <!-- String for no upstream notification disable button [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_disable_button">Turn off tethering</string>
+
+ <!-- String for cellular roaming notification title [CHAR LIMIT=200] -->
+ <string name="upstream_roaming_notification_title">Hotspot or tethering is on</string>
+ <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
+ <string name="upstream_roaming_notification_message">Additional charges may apply while roaming</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-af/strings.xml b/Tethering/res/values-mcc311-mnc480-af/strings.xml
new file mode 100644
index 0000000..9bfa531
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-af/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Verbinding het nie internet nie"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Toestelle kan nie koppel nie"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Skakel verbinding af"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Warmkol of verbinding is aan"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Bykomende heffings kan geld terwyl jy swerf"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-am/strings.xml b/Tethering/res/values-mcc311-mnc480-am/strings.xml
new file mode 100644
index 0000000..5949dfa
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-am/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ማስተሳሰር ምንም በይነመረብ የለውም"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"መሣሪያዎችን ማገናኘት አይቻልም"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ማስተሳሰርን አጥፋ"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"መገናኛ ነጥብ ወይም ማስተሳሰር በርቷል"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ar/strings.xml b/Tethering/res/values-mcc311-mnc480-ar/strings.xml
new file mode 100644
index 0000000..8467f9b
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ar/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ما مِن اتصال بالإنترنت خلال التوصيل"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"تعذّر اتصال الأجهزة"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"إيقاف التوصيل"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"نقطة الاتصال أو التوصيل مفعّلان"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"قد يتم تطبيق رسوم إضافية أثناء التجوال."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-as/strings.xml b/Tethering/res/values-mcc311-mnc480-as/strings.xml
new file mode 100644
index 0000000..9776bd8
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-as/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"টে\'ডাৰিঙৰ ইণ্টাৰনেট নাই"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"টে\'ডাৰিং অফ কৰক"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"হটস্পট অথবা টে\'ডাৰিং অন আছে"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-az/strings.xml b/Tethering/res/values-mcc311-mnc480-az/strings.xml
new file mode 100644
index 0000000..e6d3eaf
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-az/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Modemin internetə girişi yoxdur"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Cihazları qoşmaq mümkün deyil"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Modemi deaktiv edin"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot və ya modem aktivdir"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml b/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..4c8a1df
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Privezivanje nema pristup internetu"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Povezivanje uređaja nije uspelo"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Isključi privezivanje"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Uključen je hotspot ili privezivanje"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Možda važe dodatni troškovi u romingu"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-be/strings.xml b/Tethering/res/values-mcc311-mnc480-be/strings.xml
new file mode 100644
index 0000000..edfa41e
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-be/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Не ўдалося падключыць прылады"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Выключыць рэжым мадэма"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Хот-спот або рэжым мадэма ўключаны"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-bg/strings.xml b/Tethering/res/values-mcc311-mnc480-bg/strings.xml
new file mode 100644
index 0000000..f563981
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-bg/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Тетърингът няма връзка с интернет"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Устройствата не могат да установят връзка"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Изключване на тетъринга"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Точката за достъп или тетърингът са включени"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Възможно е да ви бъдат начислени допълнителни такси при роуминг"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-bn/strings.xml b/Tethering/res/values-mcc311-mnc480-bn/strings.xml
new file mode 100644
index 0000000..d8ecd2e
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-bn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"টিথারিং করার জন্য কোনও ইন্টারনেট কানেকশন নেই"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ডিভাইস কানেক্ট করতে পারছে না"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"টিথারিং বন্ধ করুন"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"হটস্পট বা টিথারিং চালু আছে"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-bs/strings.xml b/Tethering/res/values-mcc311-mnc480-bs/strings.xml
new file mode 100644
index 0000000..b85fd5e
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-bs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Povezivanje putem mobitela nema internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Uređaji se ne mogu povezati"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Isključi povezivanje putem mobitela"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Pristupna tačka ili povezivanje putem mobitela je uključeno"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Mogu nastati dodatni troškovi u romingu"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ca/strings.xml b/Tethering/res/values-mcc311-mnc480-ca/strings.xml
new file mode 100644
index 0000000..a357215
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ca/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"La compartició de xarxa no té accés a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"No es poden connectar els dispositius"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactiva la compartició de xarxa"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"S\'ha activat el punt d\'accés Wi‑Fi o la compartició de xarxa"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"És possible que s\'apliquin costos addicionals en itinerància"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-cs/strings.xml b/Tethering/res/values-mcc311-mnc480-cs/strings.xml
new file mode 100644
index 0000000..91196be
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-cs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering nemá připojení k internetu"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Zařízení se nemůžou připojit"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Vypnout tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Je zapnutý hotspot nebo tethering"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Při roamingu mohou být účtovány dodatečné poplatky"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-da/strings.xml b/Tethering/res/values-mcc311-mnc480-da/strings.xml
new file mode 100644
index 0000000..1968900
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-da/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Netdeling har ingen internetforbindelse"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Enheder kan ikke oprette forbindelse"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Deaktiver netdeling"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot eller netdeling er aktiveret"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Der opkræves muligvis yderligere gebyrer ved roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-de/strings.xml b/Tethering/res/values-mcc311-mnc480-de/strings.xml
new file mode 100644
index 0000000..eb3f8c5
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-de/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering hat keinen Internetzugriff"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Geräte können sich nicht verbinden"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tethering deaktivieren"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot oder Tethering ist aktiviert"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Für das Roaming können zusätzliche Gebühren anfallen"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-el/strings.xml b/Tethering/res/values-mcc311-mnc480-el/strings.xml
new file mode 100644
index 0000000..56c3d81
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-el/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Δεν είναι δυνατή η σύνδεση των συσκευών"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Απενεργοποιήστε τη σύνδεση"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml
new file mode 100644
index 0000000..dd1a197
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml
new file mode 100644
index 0000000..dd1a197
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml
new file mode 100644
index 0000000..dd1a197
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml
new file mode 100644
index 0000000..dd1a197
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml
new file mode 100644
index 0000000..d3347aa
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering has no internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Devices can’t connect"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Turn off tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot or tethering is on"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Additional charges may apply while roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml b/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml
new file mode 100644
index 0000000..2f0504f
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"La conexión mediante dispositivo móvil no tiene Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"No se pueden conectar los dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactivar conexión mediante dispositivo móvil"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Se activó el hotspot o la conexión mediante dispositivo móvil"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Es posible que se apliquen cargos adicionales por roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-es/strings.xml b/Tethering/res/values-mcc311-mnc480-es/strings.xml
new file mode 100644
index 0000000..2d8f882
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-es/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"La conexión no se puede compartir, porque no hay acceso a Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Los dispositivos no se pueden conectar"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactivar conexión compartida"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Punto de acceso o conexión compartida activados"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Puede que se apliquen cargos adicionales en itinerancia"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-et/strings.xml b/Tethering/res/values-mcc311-mnc480-et/strings.xml
new file mode 100644
index 0000000..8493c47
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-et/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Jagamisel puudub internetiühendus"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Seadmed ei saa ühendust luua"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Lülita jagamine välja"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Kuumkoht või jagamine on sisse lülitatud"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Rändluse kasutamisega võivad kaasneda lisatasud"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-eu/strings.xml b/Tethering/res/values-mcc311-mnc480-eu/strings.xml
new file mode 100644
index 0000000..33bccab
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-eu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Ezin dira konektatu gailuak"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desaktibatu konexioa partekatzeko aukera"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fa/strings.xml b/Tethering/res/values-mcc311-mnc480-fa/strings.xml
new file mode 100644
index 0000000..cf8a0cc
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-fa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"«اشتراکگذاری اینترنت» به اینترنت دسترسی ندارد"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"دستگاهها متصل نمیشوند"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"خاموش کردن «اشتراکگذاری اینترنت»"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"«نقطه اتصال» یا «اشتراکگذاری اینترنت» روشن است"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ممکن است درحین فراگردی تغییرات دیگر اعمال شود"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fi/strings.xml b/Tethering/res/values-mcc311-mnc480-fi/strings.xml
new file mode 100644
index 0000000..6a3ab80
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-fi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Ei jaettavaa internetyhteyttä"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Laitteet eivät voi muodostaa yhteyttä"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Laita yhteyden jakaminen pois päältä"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot tai yhteyden jakaminen on päällä"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Roaming voi aiheuttaa lisämaksuja"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml b/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml
new file mode 100644
index 0000000..ffb9bf6
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Le partage de connexion n\'est pas connecté à Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Impossible de connecter les appareils"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Désactiver le partage de connexion"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Le point d\'accès ou le partage de connexion est activé"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-fr/strings.xml b/Tethering/res/values-mcc311-mnc480-fr/strings.xml
new file mode 100644
index 0000000..768bce3
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-fr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Aucune connexion à Internet n\'est disponible pour le partage de connexion"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Impossible de connecter les appareils"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Désactiver le partage de connexion"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Le point d\'accès ou le partage de connexion est activé"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"En itinérance, des frais supplémentaires peuvent s\'appliquer"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-gl/strings.xml b/Tethering/res/values-mcc311-mnc480-gl/strings.xml
new file mode 100644
index 0000000..0c4195a
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-gl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"A conexión compartida non ten Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Non se puideron conectar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desactivar conexión compartida"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Está activada a zona wifi ou a conexión compartida"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Pódense aplicar cargos adicionais en itinerancia"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-gu/strings.xml b/Tethering/res/values-mcc311-mnc480-gu/strings.xml
new file mode 100644
index 0000000..e9d33a7
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-gu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ડિવાઇસ કનેક્ટ કરી શકાતા નથી"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hi/strings.xml b/Tethering/res/values-mcc311-mnc480-hi/strings.xml
new file mode 100644
index 0000000..aa418ac
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-hi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिंग से इंटरनेट नहीं चल रहा"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"डिवाइस कनेक्ट नहीं हो पा रहे"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिंग बंद करें"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हॉटस्पॉट या टेदरिंग चालू है"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hr/strings.xml b/Tethering/res/values-mcc311-mnc480-hr/strings.xml
new file mode 100644
index 0000000..51c524a
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-hr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Modemsko povezivanje nema internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Uređaji se ne mogu povezati"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Isključivanje modemskog povezivanja"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Uključena je žarišna točka ili modemsko povezivanje"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"U roamingu su mogući dodatni troškovi"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hu/strings.xml b/Tethering/res/values-mcc311-mnc480-hu/strings.xml
new file mode 100644
index 0000000..164e45e
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-hu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Nincs internetkapcsolat az internet megosztásához"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Az eszközök nem tudnak csatlakozni"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Internetmegosztás kikapcsolása"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"A hotspot vagy az internetmegosztás be van kapcsolva"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Roaming során további díjak léphetnek fel"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-hy/strings.xml b/Tethering/res/values-mcc311-mnc480-hy/strings.xml
new file mode 100644
index 0000000..e76c0a4
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-hy/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Մոդեմի ռեժիմի կապը բացակայում է"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Չհաջողվեց միացնել սարքը"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Անջատել մոդեմի ռեժիմը"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Թեժ կետը կամ մոդեմի ռեժիմը միացված է"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-in/strings.xml b/Tethering/res/values-mcc311-mnc480-in/strings.xml
new file mode 100644
index 0000000..2b817f8
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-in/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tidak ada koneksi internet di tethering"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Perangkat tidak dapat terhubung"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Nonaktifkan tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot atau tethering aktif"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Biaya tambahan mungkin berlaku saat roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-is/strings.xml b/Tethering/res/values-mcc311-mnc480-is/strings.xml
new file mode 100644
index 0000000..a338d9c
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-is/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tjóðrun er ekki með internettengingu"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Tæki geta ekki tengst"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Slökkva á tjóðrun"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Kveikt er á heitum reit eða tjóðrun"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Viðbótargjöld kunna að eiga við í reiki"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-it/strings.xml b/Tethering/res/values-mcc311-mnc480-it/strings.xml
new file mode 100644
index 0000000..77769c2
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-it/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Nessuna connessione a Internet per il tethering"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Impossibile connettere i dispositivi"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Disattiva il tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot o tethering attivi"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Potrebbero essere applicati costi aggiuntivi durante il roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-iw/strings.xml b/Tethering/res/values-mcc311-mnc480-iw/strings.xml
new file mode 100644
index 0000000..5267b51
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-iw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"למכשירים אין אפשרות להתחבר"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"השבתה של שיתוף האינטרנט בין מכשירים"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ייתכנו חיובים נוספים בעת נדידה"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ja/strings.xml b/Tethering/res/values-mcc311-mnc480-ja/strings.xml
new file mode 100644
index 0000000..66a9a6d
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ja/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"テザリングがインターネットに接続されていません"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"デバイスを接続できません"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"テザリングを OFF にする"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"アクセス ポイントまたはテザリングが ON です"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ローミング時に追加料金が発生することがあります"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ka/strings.xml b/Tethering/res/values-mcc311-mnc480-ka/strings.xml
new file mode 100644
index 0000000..d8ad880
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ka/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ტეტერინგს არ აქვს ინტერნეტზე წვდომა"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"მოწყობილობები ვერ ახერხებენ დაკავშირებას"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ტეტერინგის გამორთვა"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ჩართულია უსადენო ქსელი ან ტეტერინგი"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-kk/strings.xml b/Tethering/res/values-mcc311-mnc480-kk/strings.xml
new file mode 100644
index 0000000..1ddd6b4
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-kk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Тетеринг режимі интернет байланысынсыз пайдаланылуда"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Құрылғыларды байланыстыру мүмкін емес"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Тетерингіні өшіру"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Хотспот немесе тетеринг қосулы"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Роуминг кезінде қосымша ақы алынуы мүмкін."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-km/strings.xml b/Tethering/res/values-mcc311-mnc480-km/strings.xml
new file mode 100644
index 0000000..cf5a137
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-km/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ការភ្ជាប់មិនមានអ៊ីនធឺណិតទេ"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"មិនអាចភ្ជាប់ឧបករណ៍បានទេ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"បិទការភ្ជាប់"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ហតស្ប៉ត ឬការភ្ជាប់ត្រូវបានបើក"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"អាចមានការគិតថ្លៃបន្ថែម នៅពេលរ៉ូមីង"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-kn/strings.xml b/Tethering/res/values-mcc311-mnc480-kn/strings.xml
new file mode 100644
index 0000000..68ae68b
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-kn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ಟೆಥರಿಂಗ್ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ಟೆಥರಿಂಗ್ ಆಫ್ ಮಾಡಿ"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ಹಾಟ್ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್ ಆನ್ ಆಗಿದೆ"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ರೋಮಿಂಗ್ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ko/strings.xml b/Tethering/res/values-mcc311-mnc480-ko/strings.xml
new file mode 100644
index 0000000..17185ba
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ko/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"테더링으로 인터넷을 사용할 수 없음"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"기기에서 연결할 수 없음"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"테더링 사용 중지"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"핫스팟 또는 테더링 켜짐"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"로밍 중에는 추가 요금이 발생할 수 있습니다."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ky/strings.xml b/Tethering/res/values-mcc311-mnc480-ky/strings.xml
new file mode 100644
index 0000000..6a9fb98
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ky/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Модем режими Интернети жок колдонулууда"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Түзмөктөр туташпай жатат"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Модем режимин өчүрүү"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Байланыш түйүнү же модем режими күйүк"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Роумингде кошумча акы алынышы мүмкүн"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-lo/strings.xml b/Tethering/res/values-mcc311-mnc480-lo/strings.xml
new file mode 100644
index 0000000..bcc4b57
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-lo/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ປິດການປ່ອຍສັນຍານ"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ເປີດໃຊ້ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານຢູ່"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-lt/strings.xml b/Tethering/res/values-mcc311-mnc480-lt/strings.xml
new file mode 100644
index 0000000..011c2c1
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-lt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Nėra įrenginio kaip modemo naudojimo interneto ryšio"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Nepavyko susieti įrenginių"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Išjungti įrenginio kaip modemo naudojimą"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-lv/strings.xml b/Tethering/res/values-mcc311-mnc480-lv/strings.xml
new file mode 100644
index 0000000..5cb2f3b
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-lv/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Piesaistei nav interneta savienojuma"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Nevar savienot ierīces"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Izslēgt piesaisti"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ir ieslēgts tīklājs vai piesaiste"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Viesabonēšanas laikā var tikt piemērota papildu samaksa"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-mk/strings.xml b/Tethering/res/values-mcc311-mnc480-mk/strings.xml
new file mode 100644
index 0000000..4cbfd88
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-mk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Нема интернет преку мобилен"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Уредите не може да се поврзат"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Исклучи интернет преку мобилен"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Точката на пристап или интернетот преку мобилен е вклучен"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"При роаминг може да се наплатат дополнителни трошоци"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ml/strings.xml b/Tethering/res/values-mcc311-mnc480-ml/strings.xml
new file mode 100644
index 0000000..9cf4eaf
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ml/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ടെതറിംഗ് ഓഫാക്കുക"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ഹോട്ട്സ്പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-mn/strings.xml b/Tethering/res/values-mcc311-mnc480-mn/strings.xml
new file mode 100644
index 0000000..47c82c1
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-mn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Модемд интернэт алга байна"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Төхөөрөмжүүд холбогдох боломжгүй байна"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Модем болгохыг унтраах"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Сүлжээний цэг эсвэл модем болгох асаалттай байна"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-mr/strings.xml b/Tethering/res/values-mcc311-mnc480-mr/strings.xml
new file mode 100644
index 0000000..ad9e809
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-mr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिंगला इंटरनेट नाही"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"डिव्हाइस कनेक्ट होऊ शकत नाहीत"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिंग बंद करा"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हॉटस्पॉट किंवा टेदरिंग सुरू आहे"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ms/strings.xml b/Tethering/res/values-mcc311-mnc480-ms/strings.xml
new file mode 100644
index 0000000..e708cb8
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ms/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Penambatan tiada Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Peranti tidak dapat disambungkan"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Matikan penambatan"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Tempat liputan atau penambatan dihidupkan"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Caj tambahan mungkin digunakan semasa perayauan"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-my/strings.xml b/Tethering/res/values-mcc311-mnc480-my/strings.xml
new file mode 100644
index 0000000..ba54622
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-my/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းတွင် အင်တာနက် မရှိပါ"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"စက်များ ချိတ်ဆက်၍ မရပါ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ပိတ်ရန်"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ဟော့စပေါ့ (သို့) မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ဖွင့်ထားသည်"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-nb/strings.xml b/Tethering/res/values-mcc311-mnc480-nb/strings.xml
new file mode 100644
index 0000000..57db484
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-nb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Internettdeling har ikke internettilgang"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Enhetene kan ikke koble til"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Slå av internettdeling"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Wi-Fi-sone eller internettdeling er på"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ytterligere kostnader kan påløpe under roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ne/strings.xml b/Tethering/res/values-mcc311-mnc480-ne/strings.xml
new file mode 100644
index 0000000..1503244
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ne/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"यन्त्रहरू कनेक्ट गर्न सकिएन"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिङ निष्क्रिय पार्नुहोस्"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हटस्पट वा टेदरिङ सक्रिय छ"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-nl/strings.xml b/Tethering/res/values-mcc311-mnc480-nl/strings.xml
new file mode 100644
index 0000000..b08133f
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-nl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering heeft geen internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Apparaten kunnen niet worden verbonden"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tethering uitschakelen"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot of tethering is ingeschakeld"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Er kunnen extra kosten voor roaming in rekening worden gebracht."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-or/strings.xml b/Tethering/res/values-mcc311-mnc480-or/strings.xml
new file mode 100644
index 0000000..1ad4ca3
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-or/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ଡିଭାଇସଗୁଡ଼ିକ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ଟିଥରିଂ ବନ୍ଦ କରନ୍ତୁ"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ହଟସ୍ପଟ୍ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pa/strings.xml b/Tethering/res/values-mcc311-mnc480-pa/strings.xml
new file mode 100644
index 0000000..88def56
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-pa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pl/strings.xml b/Tethering/res/values-mcc311-mnc480-pl/strings.xml
new file mode 100644
index 0000000..f9890ab
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-pl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering nie ma internetu"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Urządzenia nie mogą się połączyć"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Wyłącz tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot lub tethering jest włączony"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml b/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml
new file mode 100644
index 0000000..ce3b884
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"O tethering não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Não é possível conectar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desativar o tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ponto de acesso ou tethering ativado"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Pode haver cobranças extras durante o roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml b/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml
new file mode 100644
index 0000000..7e883ea
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"A ligação (à Internet) via telemóvel não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Não é possível ligar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desativar ligação (à Internet) via telemóvel"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Podem aplicar-se custos adicionais em roaming."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-pt/strings.xml b/Tethering/res/values-mcc311-mnc480-pt/strings.xml
new file mode 100644
index 0000000..ce3b884
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-pt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"O tethering não tem Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Não é possível conectar os dispositivos"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Desativar o tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Ponto de acesso ou tethering ativado"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Pode haver cobranças extras durante o roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ro/strings.xml b/Tethering/res/values-mcc311-mnc480-ro/strings.xml
new file mode 100644
index 0000000..1009417
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ro/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Procesul de tethering nu are internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Dispozitivele nu se pot conecta"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Dezactivați procesul de tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"S-a activat hotspotul sau tethering"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Se pot aplica taxe suplimentare pentru roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ru/strings.xml b/Tethering/res/values-mcc311-mnc480-ru/strings.xml
new file mode 100644
index 0000000..88683be
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ru/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Режим модема используется без доступа к Интернету"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Невозможно подключить устройства."</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Отключить режим модема"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Включены точка доступа или режим модема"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"За использование услуг связи в роуминге может взиматься дополнительная плата."</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-si/strings.xml b/Tethering/res/values-mcc311-mnc480-si/strings.xml
new file mode 100644
index 0000000..176bcdb
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-si/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ටෙදරින් හට අන්තර්ජාලය නැත"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"උපාංගවලට සම්බන්ධ විය නොහැකිය"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ටෙදරින් ක්රියාවිරහිත කරන්න"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"හොට්ස්පොට් හෝ ටෙදරින් ක්රියාත්මකයි"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sk/strings.xml b/Tethering/res/values-mcc311-mnc480-sk/strings.xml
new file mode 100644
index 0000000..b9e2127
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-sk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering nemá internetové pripojenie"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Zariadenia sa nemôžu pripojiť"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Vypnúť tethering"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Je zapnutý hotspot alebo tethering"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Počas roamingu vám môžu byť účtované ďalšie poplatky"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sl/strings.xml b/Tethering/res/values-mcc311-mnc480-sl/strings.xml
new file mode 100644
index 0000000..e8140e6
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-sl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Internetna povezava prek mobilnega telefona ni vzpostavljena"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Napravi se ne moreta povezati"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Izklopi internetno povezavo prek mobilnega telefona"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Dostopna točka ali internetna povezava prek mobilnega telefona je vklopljena"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Med gostovanjem lahko nastanejo dodatni stroški"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sq/strings.xml b/Tethering/res/values-mcc311-mnc480-sq/strings.xml
new file mode 100644
index 0000000..61e698d
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-sq/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Ndarja e internetit nuk ka internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Pajisjet nuk mund të lidhen"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Çaktivizo ndarjen e internetit"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Zona e qasjes për internet ose ndarja e internetit është aktive"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Mund të zbatohen tarifime shtesë kur je në roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sr/strings.xml b/Tethering/res/values-mcc311-mnc480-sr/strings.xml
new file mode 100644
index 0000000..b4c411c
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-sr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Привезивање нема приступ интернету"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Повезивање уређаја није успело"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Искључи привезивање"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Укључен је хотспот или привезивање"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Можда важе додатни трошкови у ромингу"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sv/strings.xml b/Tethering/res/values-mcc311-mnc480-sv/strings.xml
new file mode 100644
index 0000000..4f543e4
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-sv/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Det finns ingen internetanslutning för internetdelningen"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Enheterna kan inte anslutas"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Inaktivera internetdelning"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Surfzon eller internetdelning har aktiverats"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Ytterligare avgifter kan tillkomma vid roaming"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-sw/strings.xml b/Tethering/res/values-mcc311-mnc480-sw/strings.xml
new file mode 100644
index 0000000..ac347ab
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-sw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Kipengele cha kusambaza mtandao hakina intaneti"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Imeshindwa kuunganisha vifaa"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Zima kipengele cha kusambaza mtandao"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Umewasha kipengele cha kusambaza mtandao au mtandao pepe"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ta/strings.xml b/Tethering/res/values-mcc311-mnc480-ta/strings.xml
new file mode 100644
index 0000000..2ea2467
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ta/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"சாதனங்களால் இணைய முடியவில்லை"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"இணைப்பு முறையை ஆஃப் செய்"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-te/strings.xml b/Tethering/res/values-mcc311-mnc480-te/strings.xml
new file mode 100644
index 0000000..9360297
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-te/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"పరికరాలు కనెక్ట్ అవ్వడం లేదు"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"టెథరింగ్ను ఆఫ్ చేయండి"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"హాట్స్పాట్ లేదా టెథరింగ్ ఆన్లో ఉంది"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"రోమింగ్లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-th/strings.xml b/Tethering/res/values-mcc311-mnc480-th/strings.xml
new file mode 100644
index 0000000..9c4d7e0
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-th/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือไม่มีอินเทอร์เน็ต"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"อุปกรณ์เชื่อมต่อไม่ได้"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-tl/strings.xml b/Tethering/res/values-mcc311-mnc480-tl/strings.xml
new file mode 100644
index 0000000..a7c78a5
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-tl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Walang internet ang pag-tether"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Hindi makakonekta ang mga device"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"I-off ang pag-tether"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Naka-on ang Hotspot o pag-tether"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Posibleng magkaroon ng mga karagdagang singil habang nagro-roam"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-tr/strings.xml b/Tethering/res/values-mcc311-mnc480-tr/strings.xml
new file mode 100644
index 0000000..93da2c3
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-tr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Tethering\'in internet bağlantısı yok"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Cihazlar bağlanamıyor"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tethering\'i kapat"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot veya tethering açık"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Dolaşım sırasında ek ücretler uygulanabilir"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-uk/strings.xml b/Tethering/res/values-mcc311-mnc480-uk/strings.xml
new file mode 100644
index 0000000..ee0dcd2
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-uk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Телефон, який використовується як модем, не підключений до Інтернету"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Не вдається підключити пристрої"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Вимкнути використання телефона як модема"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Увімкнено точку доступу або використання телефона як модема"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"У роумінгу може стягуватися додаткова плата"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ur/strings.xml b/Tethering/res/values-mcc311-mnc480-ur/strings.xml
new file mode 100644
index 0000000..41cd28e
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-ur/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"ٹیدرنگ میں انٹرنیٹ نہیں ہے"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"آلات منسلک نہیں ہو سکتے"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"ٹیدرنگ آف کریں"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ہاٹ اسپاٹ یا ٹیدرنگ آن ہے"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-uz/strings.xml b/Tethering/res/values-mcc311-mnc480-uz/strings.xml
new file mode 100644
index 0000000..c847bc9
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-uz/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Modem internetga ulanmagan"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Qurilmalar ulanmadi"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Modem rejimini faolsizlantirish"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Hotspot yoki modem rejimi yoniq"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Rouming vaqtida qoʻshimcha haq olinishi mumkin"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-vi/strings.xml b/Tethering/res/values-mcc311-mnc480-vi/strings.xml
new file mode 100644
index 0000000..a74326f
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-vi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Không có Internet để chia sẻ kết Internet"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Các thiết bị không thể kết nối"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Tắt tính năng chia sẻ Internet"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Bạn có thể mất thêm phí dữ liệu khi chuyển vùng"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml
new file mode 100644
index 0000000..d737003
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"共享网络未连接到互联网"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"设备无法连接"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"关闭网络共享"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"热点或网络共享已开启"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"漫游时可能会产生额外的费用"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml
new file mode 100644
index 0000000..f378a9d
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"無法透過網絡共享連線至互聯網"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"裝置無法連接"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"關閉網絡共享"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"熱點或網絡共享已開啟"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"漫遊時可能需要支付額外費用"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
new file mode 100644
index 0000000..cd653df
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"無法透過網路共用連上網際網路"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"裝置無法連線"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"關閉網路共用"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"無線基地台或網路共用已開啟"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"使用漫遊服務可能須支付額外費用"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zu/strings.xml b/Tethering/res/values-mcc311-mnc480-zu/strings.xml
new file mode 100644
index 0000000..32f6df5
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480-zu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="no_upstream_notification_title" msgid="611650570559011140">"Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi"</string>
+ <string name="no_upstream_notification_message" msgid="6508394877641864863">"Amadivayisi awakwazi ukuxhumeka"</string>
+ <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"Vala ukusebenzisa ifoni njengemodemu"</string>
+ <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe"</string>
+ <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"Kungaba nezinkokhelo ezengeziwe uma uzula"</string>
+</resources>
diff --git a/Tethering/res/values-mcc311-mnc480/config.xml b/Tethering/res/values-mcc311-mnc480/config.xml
new file mode 100644
index 0000000..5c5be04
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
+ "0" for disable this feature. -->
+ <integer name="delay_to_show_no_upstream_after_no_backhaul">5000</integer>
+
+ <!-- Config for showing upstream roaming notification. -->
+ <bool name="config_upstream_roaming_notification">true</bool>
+</resources>
\ No newline at end of file
diff --git a/Tethering/res/values-mcc311-mnc480/strings.xml b/Tethering/res/values-mcc311-mnc480/strings.xml
new file mode 100644
index 0000000..ce9ff60
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- String for no upstream notification title [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_title">Tethering has no internet</string>
+ <!-- String for no upstream notification title [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_message">Devices can\u2019t connect</string>
+ <!-- String for no upstream notification disable button [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_disable_button">Turn off tethering</string>
+
+ <!-- String for cellular roaming notification title [CHAR LIMIT=200] -->
+ <string name="upstream_roaming_notification_title">Hotspot or tethering is on</string>
+ <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
+ <string name="upstream_roaming_notification_message">Additional charges may apply while roaming</string>
+</resources>
diff --git a/Tethering/res/values-mk/strings.xml b/Tethering/res/values-mk/strings.xml
new file mode 100644
index 0000000..9ad9b9a
--- /dev/null
+++ b/Tethering/res/values-mk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Активно е врзување или точка на пристап"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Допрете за поставување."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Врзувањето е оневозможено"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Контактирајте со администраторот за детали"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус на точката на пристап и врзувањето"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ml/strings.xml b/Tethering/res/values-ml/strings.xml
new file mode 100644
index 0000000..9db79ce
--- /dev/null
+++ b/Tethering/res/values-ml/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ടെതറിംഗ് അല്ലെങ്കിൽ ഹോട്ട്സ്പോട്ട് സജീവമാണ്"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"സജ്ജീകരിക്കാൻ ടാപ്പ് ചെയ്യുക."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ടെതറിംഗ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"വിശദാംശങ്ങൾക്ക് നിങ്ങളുടെ അഡ്മിനെ ബന്ധപ്പെടുക"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ഹോട്ട്സ്പോട്ടിന്റെയും ടെതറിംഗിന്റെയും നില"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-mn/strings.xml b/Tethering/res/values-mn/strings.xml
new file mode 100644
index 0000000..42d1edb
--- /dev/null
+++ b/Tethering/res/values-mn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Модем болгох эсвэл сүлжээний цэг идэвхтэй байна"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Тохируулахын тулд товшино уу."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Модем болгохыг идэвхгүй болгосон"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Дэлгэрэнгүй мэдээлэл авахын тулд админтайгаа холбогдоно уу"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Сүлжээний цэг болон модем болгох төлөв"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-mr/strings.xml b/Tethering/res/values-mr/strings.xml
new file mode 100644
index 0000000..13995b6
--- /dev/null
+++ b/Tethering/res/values-mr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"टेदरिंग किंवा हॉटस्पॉट अॅक्टिव्ह आहे"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"सेट करण्यासाठी टॅप करा."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"टेदरिंग बंद केले आहे"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"तपशीलांसाठी तुमच्या ॲडमिनशी संपर्क साधा"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"हॉटस्पॉट आणि टेदरिंगची स्थिती"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ms/strings.xml b/Tethering/res/values-ms/strings.xml
new file mode 100644
index 0000000..d6a67f3
--- /dev/null
+++ b/Tethering/res/values-ms/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Penambatan atau tempat liputan aktif"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Ketik untuk membuat persediaan."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Penambatan dilumpuhkan"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hubungi pentadbir anda untuk mendapatkan maklumat lanjut"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status tempat liputan & penambatan"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-my/strings.xml b/Tethering/res/values-my/strings.xml
new file mode 100644
index 0000000..49f6b88
--- /dev/null
+++ b/Tethering/res/values-my/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း သို့မဟုတ် ဟော့စပေါ့ ဖွင့်ထားသည်"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"စနစ်ထည့်သွင်းရန် တို့ပါ။"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းကို ပိတ်ထားသည်"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"အသေးစိတ်အတွက် သင့်စီမံခန့်ခွဲသူကို ဆက်သွယ်ပါ"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ဟော့စပေါ့နှင့် မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း အခြေအနေ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-nb/strings.xml b/Tethering/res/values-nb/strings.xml
new file mode 100644
index 0000000..9594e0a
--- /dev/null
+++ b/Tethering/res/values-nb/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Internettdeling eller Wi-Fi-sone er aktiv"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Trykk for å konfigurere."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Internettdeling er slått av"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Ta kontakt med administratoren din for å få mer informasjon"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status for Wi-Fi-sone og internettdeling"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ne/strings.xml b/Tethering/res/values-ne/strings.xml
new file mode 100644
index 0000000..72ae3a8
--- /dev/null
+++ b/Tethering/res/values-ne/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"टेदरिङ वा हटस्पट सक्रिय छ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"सेटअप गर्न ट्याप गर्नुहोस्।"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"टेदरिङ सुविधा असक्षम पारिएको छ"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"विवरणहरूका लागि आफ्ना प्रशासकलाई सम्पर्क गर्नुहोस्"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"हटस्पट तथा टेदरिङको स्थिति"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-nl/strings.xml b/Tethering/res/values-nl/strings.xml
new file mode 100644
index 0000000..18b2bbf
--- /dev/null
+++ b/Tethering/res/values-nl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering of hotspot actief"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tik om in te stellen."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering is uitgeschakeld"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Neem contact op met je beheerder voor meer informatie"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status van hotspot en tethering"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-or/strings.xml b/Tethering/res/values-or/strings.xml
new file mode 100644
index 0000000..a15a6db
--- /dev/null
+++ b/Tethering/res/values-or/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ଟିଥେରିଂ କିମ୍ୱା ହଟସ୍ପଟ୍ ସକ୍ରିୟ ଅଛି"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ସେଟ୍ ଅପ୍ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ।"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ଟିଥେରିଂ ଅକ୍ଷମ କରାଯାଇଛି"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ବିବରଣୀଗୁଡ଼ିକ ପାଇଁ ଆପଣଙ୍କ ଆଡମିନଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ହଟସ୍ପଟ୍ ଓ ଟିଥେରିଂ ସ୍ଥିତି"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-pa/strings.xml b/Tethering/res/values-pa/strings.xml
new file mode 100644
index 0000000..a8235e4
--- /dev/null
+++ b/Tethering/res/values-pa/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ਟੈਦਰਿੰਗ ਜਾਂ ਹੌਟਸਪੌਟ ਕਿਰਿਆਸ਼ੀਲ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"ਸੈੱਟਅੱਪ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ਟੈਦਰਿੰਗ ਨੂੰ ਬੰਦ ਕੀਤਾ ਗਿਆ ਹੈ"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ਵੇਰਵਿਆਂ ਲਈ ਆਪਣੇ ਪ੍ਰਸ਼ਾਸਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ਹੌਟਸਪੌਟ ਅਤੇ ਟੈਦਰਿੰਗ ਦੀ ਸਥਿਤੀ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-pl/strings.xml b/Tethering/res/values-pl/strings.xml
new file mode 100644
index 0000000..ccb017d
--- /dev/null
+++ b/Tethering/res/values-pl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Aktywny tethering lub punkt dostępu"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Kliknij, by skonfigurować"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering został wyłączony"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Aby uzyskać szczegółowe informacje, skontaktuj się z administratorem"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot i tethering – stan"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-pt-rBR/strings.xml b/Tethering/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..a0a4745
--- /dev/null
+++ b/Tethering/res/values-pt-rBR/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Ponto de acesso ou tethering ativo"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Toque para configurar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering desativado"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Fale com seu administrador para saber detalhes"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status de ponto de acesso e tethering"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-pt-rPT/strings.xml b/Tethering/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..e3f03fc
--- /dev/null
+++ b/Tethering/res/values-pt-rPT/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Ligação (à Internet) via telemóvel ou zona Wi-Fi ativas"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Toque para configurar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"A ligação (à Internet) via telemóvel está desativada."</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contacte o administrador para obter detalhes."</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Estado da zona Wi-Fi e da ligação (à Internet) via telemóvel"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-pt/strings.xml b/Tethering/res/values-pt/strings.xml
new file mode 100644
index 0000000..a0a4745
--- /dev/null
+++ b/Tethering/res/values-pt/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Ponto de acesso ou tethering ativo"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Toque para configurar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering desativado"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Fale com seu administrador para saber detalhes"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status de ponto de acesso e tethering"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ro/strings.xml b/Tethering/res/values-ro/strings.xml
new file mode 100644
index 0000000..5706a4a
--- /dev/null
+++ b/Tethering/res/values-ro/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering sau hotspot activ"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Atingeți ca să configurați."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tetheringul este dezactivat"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Contactați administratorul pentru detalii"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Starea hotspotului și a tetheringului"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ru/strings.xml b/Tethering/res/values-ru/strings.xml
new file mode 100644
index 0000000..7cb6f7d
--- /dev/null
+++ b/Tethering/res/values-ru/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Включен режим модема или точка доступа"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Нажмите, чтобы настроить."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Использование телефона в качестве модема запрещено"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Чтобы узнать подробности, обратитесь к администратору."</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус хот-спота и режима модема"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-si/strings.xml b/Tethering/res/values-si/strings.xml
new file mode 100644
index 0000000..ec34c22
--- /dev/null
+++ b/Tethering/res/values-si/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ටෙදරින් හෝ හොට්ස්පොට් සක්රීයයි"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"පිහිටුවීමට තට්ටු කරන්න."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ටෙදරින් අබල කර ඇත"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"විස්තර සඳහා ඔබගේ පරිපාලක අමතන්න"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"හොට්ස්පොට් & ටෙදරින් තත්ත්වය"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-sk/strings.xml b/Tethering/res/values-sk/strings.xml
new file mode 100644
index 0000000..43e787c
--- /dev/null
+++ b/Tethering/res/values-sk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering alebo prístupový bod je aktívny"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Klepnutím prejdete na nastavenie."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering je deaktivovaný"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"O podrobnosti požiadajte svojho správcu"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stav hotspotu a tetheringu"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-sl/strings.xml b/Tethering/res/values-sl/strings.xml
new file mode 100644
index 0000000..5943362
--- /dev/null
+++ b/Tethering/res/values-sl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Povezava z internetom prek mobilnega telefona ali dostopna točka je aktivna"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Dotaknite se, če želite nastaviti."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Povezava z internetom prek mobilnega telefona je onemogočena"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Za podrobnosti se obrnite na skrbnika"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Stanje dostopne točke in povezave z internetom prek mobilnega telefona"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-sq/strings.xml b/Tethering/res/values-sq/strings.xml
new file mode 100644
index 0000000..21e1155
--- /dev/null
+++ b/Tethering/res/values-sq/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Ndarja e internetit ose zona e qasjes së internetit është aktive"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Trokit për ta konfiguruar."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Ndarja e internetit është çaktivizuar"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontakto me administratorin për detaje"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Statusi i zonës së qasjes dhe ndarjes së internetit"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-sr/strings.xml b/Tethering/res/values-sr/strings.xml
new file mode 100644
index 0000000..e2e4dc6
--- /dev/null
+++ b/Tethering/res/values-sr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Привезивање или хотспот је активан"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Додирните да бисте подесили."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Привезивање је онемогућено"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Потражите детаље од администратора"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус хотспота и привезивања"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-sv/strings.xml b/Tethering/res/values-sv/strings.xml
new file mode 100644
index 0000000..72702c2
--- /dev/null
+++ b/Tethering/res/values-sv/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Internetdelning eller surfzon har aktiverats"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Tryck om du vill konfigurera."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Internetdelning har inaktiverats"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Kontakta administratören om du vill veta mer"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Trådlös surfzon och internetdelning har inaktiverats"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-sw/strings.xml b/Tethering/res/values-sw/strings.xml
new file mode 100644
index 0000000..65e4aa8
--- /dev/null
+++ b/Tethering/res/values-sw/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Kusambaza mtandao au mtandaopepe umewashwa"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Gusa ili uweke mipangilio."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Umezima kipengele cha kusambaza mtandao"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Wasiliana na msimamizi wako ili upate maelezo zaidi"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Mtandaopepe na hali ya kusambaza mtandao"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ta/strings.xml b/Tethering/res/values-ta/strings.xml
new file mode 100644
index 0000000..4aba62d
--- /dev/null
+++ b/Tethering/res/values-ta/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"டெதெரிங் அல்லது ஹாட்ஸ்பாட் இயங்குகிறது"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"அமைக்க, தட்டவும்."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"டெதெரிங் முடக்கப்பட்டுள்ளது"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"விவரங்களுக்கு உங்கள் நிர்வாகியைத் தொடர்புகொள்ளவும்"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ஹாட்ஸ்பாட் & டெதெரிங் நிலை"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-te/strings.xml b/Tethering/res/values-te/strings.xml
new file mode 100644
index 0000000..1f91791
--- /dev/null
+++ b/Tethering/res/values-te/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"టెథరింగ్ లేదా హాట్స్పాట్ యాక్టివ్గా ఉంది"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"సెటప్ చేయడానికి ట్యాప్ చేయండి."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"టెథరింగ్ డిజేబుల్ చేయబడింది"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"వివరాల కోసం మీ అడ్మిన్ని సంప్రదించండి"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"హాట్స్పాట్ & టెథరింగ్ స్థితి"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-th/strings.xml b/Tethering/res/values-th/strings.xml
new file mode 100644
index 0000000..44171c0
--- /dev/null
+++ b/Tethering/res/values-th/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือหรือฮอตสปอตทำงานอยู่"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"แตะเพื่อตั้งค่า"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ปิดใช้การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือแล้ว"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"ติดต่อผู้ดูแลระบบเพื่อขอรายละเอียด"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"สถานะฮอตสปอตและการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-tl/strings.xml b/Tethering/res/values-tl/strings.xml
new file mode 100644
index 0000000..7347dd3
--- /dev/null
+++ b/Tethering/res/values-tl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Aktibo ang pag-tether o hotspot"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"I-tap para i-set up."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Naka-disable ang pag-tether"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Makipag-ugnayan sa iyong admin para sa mga detalye"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Status ng hotspot at pag-tether"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-tr/strings.xml b/Tethering/res/values-tr/strings.xml
new file mode 100644
index 0000000..32030f1
--- /dev/null
+++ b/Tethering/res/values-tr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tethering veya hotspot etkin"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Ayarlamak için dokunun."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Tethering devre dışı bırakıldı"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Ayrıntılı bilgi için yöneticinize başvurun"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot ve tethering durumu"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-uk/strings.xml b/Tethering/res/values-uk/strings.xml
new file mode 100644
index 0000000..1ca89b3
--- /dev/null
+++ b/Tethering/res/values-uk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Модем чи точка доступу активні"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Натисніть, щоб налаштувати."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Використання телефона як модема вимкнено"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Щоб дізнатися більше, зв\'яжіться з адміністратором"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Статус точки доступу та модема"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-ur/strings.xml b/Tethering/res/values-ur/strings.xml
new file mode 100644
index 0000000..d72c7d4
--- /dev/null
+++ b/Tethering/res/values-ur/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"ٹیدرنگ یا ہاٹ اسپاٹ فعال"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"سیٹ اپ کرنے کیلئے تھپتھپائیں۔"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"ٹیدرنگ غیر فعال ہے"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"تفصیلات کے لئے اپنے منتظم سے رابطہ کریں"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"ہاٹ اسپاٹ اور ٹیتھرنگ کا اسٹیٹس"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-uz/strings.xml b/Tethering/res/values-uz/strings.xml
new file mode 100644
index 0000000..af3b2eb
--- /dev/null
+++ b/Tethering/res/values-uz/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Modem rejimi yoki hotspot yoniq"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Sozlash uchun bosing."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Modem rejimi faolsizlantirildi"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Tafsilotlari uchun administratoringizga murojaat qiling"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Hotspot va modem rejimi holati"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-vi/strings.xml b/Tethering/res/values-vi/strings.xml
new file mode 100644
index 0000000..21a0735
--- /dev/null
+++ b/Tethering/res/values-vi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Tính năng chia sẻ Internet hoặc điểm phát sóng đang hoạt động"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Hãy nhấn để thiết lập."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Đã tắt tính năng chia sẻ Internet"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Hãy liên hệ với quản trị viên của bạn để biết chi tiết"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"Trạng thái điểm phát sóng và chia sẻ Internet"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-zh-rCN/strings.xml b/Tethering/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..98e3b4b
--- /dev/null
+++ b/Tethering/res/values-zh-rCN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"网络共享或热点已启用"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"点按即可设置。"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"网络共享已停用"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"如需了解详情,请与您的管理员联系"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"热点和网络共享状态"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-zh-rHK/strings.xml b/Tethering/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..9cafd42
--- /dev/null
+++ b/Tethering/res/values-zh-rHK/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"網絡共享或熱點已啟用"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"輕按即可設定。"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"網絡共享已停用"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"請聯絡您的管理員以瞭解詳情"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"熱點和網絡共享狀態"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-zh-rTW/strings.xml b/Tethering/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..50a50bf
--- /dev/null
+++ b/Tethering/res/values-zh-rTW/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"網路共用或無線基地台已啟用"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"輕觸即可進行設定。"</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"網路共用已停用"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"詳情請洽你的管理員"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"無線基地台與網路共用狀態"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values-zu/strings.xml b/Tethering/res/values-zu/strings.xml
new file mode 100644
index 0000000..f210f87
--- /dev/null
+++ b/Tethering/res/values-zu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="tethered_notification_title" msgid="6426563586025792944">"Ukusebenzisa njengemodemu noma i-hotspot ephathekayo kuvuliwe"</string>
+ <string name="tethered_notification_message" msgid="64800879503420696">"Thepha ukuze usethe."</string>
+ <string name="disable_tether_notification_title" msgid="3004509127903564191">"Ukusebenzisa ifoni njengemodemu kukhutshaziwe"</string>
+ <string name="disable_tether_notification_message" msgid="6717523799293901476">"Xhumana nomphathi wakho ukuze uthole imininingwane"</string>
+ <string name="notification_channel_tethering_status" msgid="2663463891530932727">"I-Hotspot nesimo sokusebenzisa ifoni njengemodemu"</string>
+ <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
+ <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
+ <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
+ <string name="upstream_roaming_notification_title" msgid="4772373823198997030"></string>
+ <string name="upstream_roaming_notification_message" msgid="3985577843181551650"></string>
+</resources>
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
new file mode 100644
index 0000000..4391006
--- /dev/null
+++ b/Tethering/res/values/config.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!--
+ OEMs that wish to change the below settings must do so via a runtime resource overlay package
+ and *NOT* by changing this file. This file is part of the tethering mainline module.
+ TODO: define two resources for each config item: a default_* resource and a config_* resource,
+ config_* is empty by default but may be overridden by RROs.
+ -->
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
+ USB interfaces. If the device doesn't want to support tethering over USB this should
+ be empty. An example would be "usb.*" -->
+ <string-array translatable="false" name="config_tether_usb_regexs">
+ <item>"usb\\d"</item>
+ <item>"rndis\\d"</item>
+ </string-array>
+
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
+ NCM interfaces. If the device doesn't want to support tethering over NCM this should
+ be empty. -->
+ <string-array translatable="false" name="config_tether_ncm_regexs">
+ </string-array>
+
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
+ Wifi interfaces. If the device doesn't want to support tethering over Wifi this
+ should be empty. An example would be "softap.*" -->
+ <string-array translatable="false" name="config_tether_wifi_regexs">
+ <item>"wlan\\d"</item>
+ <item>"softap\\d"</item>
+ <item>"ap_br_wlan\\d"</item>
+ <item>"ap_br_softap\\d"</item>
+ </string-array>
+
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
+ WiGig interfaces. If the device doesn't want to support tethering over WiGig this
+ should be empty. An example would be "wigig\\d" -->
+ <string-array translatable="false" name="config_tether_wigig_regexs">
+ <item>"wigig\\d"</item>
+ </string-array>
+
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
+ Wifi P2P interfaces. If the device doesn't want to support tethering over Wifi P2p this
+ should be empty. An example would be "p2p-p2p\\d-.*" -->
+ <string-array translatable="false" name="config_tether_wifi_p2p_regexs">
+ <item>"p2p-p2p\\d-.*"</item>
+ <item>"p2p\\d"</item>
+ </string-array>
+
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
+ bluetooth interfaces. If the device doesn't want to support tethering over bluetooth this
+ should be empty. -->
+ <string-array translatable="false" name="config_tether_bluetooth_regexs">
+ <item>"bt-pan"</item>
+ </string-array>
+
+ <!-- Use the BPF offload for tethering when the kernel has support. True by default.
+ If the device doesn't want to support tether BPF offload, this should be false.
+ Note that this setting could be overridden by device config.
+ -->
+ <bool translatable="false" name="config_tether_enable_bpf_offload">true</bool>
+
+ <!-- Use the old dnsmasq DHCP server for tethering instead of the framework implementation. -->
+ <bool translatable="false" name="config_tether_enable_legacy_dhcp_server">false</bool>
+
+ <!-- Use legacy wifi p2p dedicated address instead of randomize address. -->
+ <bool translatable="false" name="config_tether_enable_legacy_wifi_p2p_dedicated_ip">false</bool>
+
+ <!-- Dhcp range (min, max) to use for tethering purposes -->
+ <string-array translatable="false" name="config_tether_dhcp_range">
+ </string-array>
+
+ <!-- Used to config periodic polls tether offload stats from tethering offload HAL to make the
+ data warnings work. 5000(ms) by default. If the device doesn't want to poll tether
+ offload stats, this should be -1. Note that this setting could be override by
+ runtime resource overlays.
+ -->
+ <integer translatable="false" name="config_tether_offload_poll_interval">5000</integer>
+
+ <!-- Array of ConnectivityManager.TYPE_{BLUETOOTH, ETHERNET, MOBILE, MOBILE_DUN, MOBILE_HIPRI,
+ WIFI} values allowable for tethering.
+
+ Common options are [1, 4] for TYPE_WIFI and TYPE_MOBILE_DUN or
+ [1,7,0] for TYPE_WIFI, TYPE_BLUETOOTH, and TYPE_MOBILE.
+
+ This list is also modified by code within the framework, including:
+
+ - TYPE_ETHERNET (9) is prepended to this list, and
+
+ - the return value of TelephonyManager.isTetheringApnRequired()
+ determines how the array is further modified:
+
+ * TRUE (DUN REQUIRED).
+ TYPE_MOBILE is removed (if present).
+ TYPE_MOBILE_HIPRI is removed (if present).
+ TYPE_MOBILE_DUN is appended (if not already present).
+
+ * FALSE (DUN NOT REQUIRED).
+ TYPE_MOBILE_DUN is removed (if present).
+ If both of TYPE_MOBILE{,_HIPRI} are not present:
+ TYPE_MOBILE is appended.
+ TYPE_MOBILE_HIPRI is appended.
+
+ For other changes applied to this list, now and in the future, see
+ com.android.networkstack.tethering.TetheringConfiguration.
+
+ Note also: the order of this is important. The first upstream type
+ for which a satisfying network exists is used.
+ -->
+ <integer-array translatable="false" name="config_tether_upstream_types">
+ </integer-array>
+
+ <!-- When true, the tethering upstream network follows the current default
+ Internet network (except when the current default network is mobile,
+ in which case a DUN network will be used if required).
+
+ When true, overrides the config_tether_upstream_types setting above.
+ -->
+ <bool translatable="false" name="config_tether_upstream_automatic">true</bool>
+
+
+ <!-- If the mobile hotspot feature requires provisioning, a package name and class name
+ can be provided to launch a supported application that provisions the devices.
+ EntitlementManager will send an intent to Settings with the specified package name and
+ class name in extras to launch provision app.
+ TODO: note what extras here.
+
+ See EntitlementManager#runUiTetherProvisioning and
+ packages/apps/Settings/src/com/android/settings/network/TetherProvisioningActivity.java
+ for more details.
+
+ For ui-less/periodic recheck support see config_mobile_hotspot_provision_app_no_ui
+ -->
+ <!-- The first element is the package name and the second element is the class name
+ of the provisioning app -->
+ <string-array translatable="false" name="config_mobile_hotspot_provision_app">
+ <!--
+ <item>com.example.provisioning</item>
+ <item>com.example.provisioning.Activity</item>
+ -->
+ </string-array>
+
+ <!-- If the mobile hotspot feature requires provisioning, an action can be provided
+ that will be broadcast in non-ui cases for checking the provisioning status.
+ EntitlementManager will pass the specified name to Settings and Settings would
+ launch provisioning app by sending an intent with the package name.
+
+ A second broadcast, action defined by config_mobile_hotspot_provision_response,
+ will be sent back to notify if provisioning succeeded or not. The response will
+ match that of the activity in config_mobile_hotspot_provision_app, but instead
+ contained within the int extra "EntitlementResult".
+ TODO: provide the system api for "EntitlementResult" extra and note it here.
+
+ See EntitlementManager#runSilentTetherProvisioning and
+ packages/apps/Settings/src/com/android/settings/wifi/tether/TetherService.java for more
+ details.
+ -->
+ <string translatable="false" name="config_mobile_hotspot_provision_app_no_ui"></string>
+
+ <!-- Sent in response to a provisioning check. The caller must hold the
+ permission android.permission.TETHER_PRIVILEGED for Settings to
+ receive this response.
+
+ See config_mobile_hotspot_provision_response
+ -->
+ <string translatable="false" name="config_mobile_hotspot_provision_response"></string>
+
+ <!-- Number of hours between each background provisioning call -->
+ <integer translatable="false" name="config_mobile_hotspot_provision_check_period">24</integer>
+
+ <!-- ComponentName of the service used to run no ui tether provisioning. -->
+ <string translatable="false" name="config_wifi_tether_enable">com.android.settings/.wifi.tether.TetherService</string>
+
+ <!-- No upstream notification is shown when there is a downstream but no upstream that is able
+ to do the tethering. -->
+ <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
+ "-1" for disable this feature. -->
+ <integer name="delay_to_show_no_upstream_after_no_backhaul">-1</integer>
+
+ <!-- Cellular roaming notification is shown when upstream is cellular network and in roaming
+ state. -->
+ <!-- Config for showing upstream roaming notification. -->
+ <bool name="config_upstream_roaming_notification">false</bool>
+</resources>
diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml
new file mode 100644
index 0000000..0ee7a99
--- /dev/null
+++ b/Tethering/res/values/overlayable.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <overlayable name="TetheringConfig">
+ <policy type="product|system|vendor">
+ <!-- Params from config.xml that can be overlaid -->
+ <item type="array" name="config_tether_usb_regexs"/>
+ <item type="array" name="config_tether_ncm_regexs" />
+ <item type="array" name="config_tether_wifi_regexs"/>
+ <item type="array" name="config_tether_wigig_regexs"/>
+ <item type="array" name="config_tether_wifi_p2p_regexs"/>
+ <item type="array" name="config_tether_bluetooth_regexs"/>
+ <item type="array" name="config_tether_dhcp_range"/>
+ <!-- Use the BPF offload for tethering when the kernel has support. True by default.
+ If the device doesn't want to support tether BPF offload, this should be false.
+ Note that this setting could be overridden by device config.
+ -->
+ <item type="bool" name="config_tether_enable_bpf_offload"/>
+ <item type="bool" name="config_tether_enable_legacy_dhcp_server"/>
+ <item type="bool" name="config_tether_enable_legacy_wifi_p2p_dedicated_ip"/>
+ <item type="integer" name="config_tether_offload_poll_interval"/>
+ <item type="array" name="config_tether_upstream_types"/>
+ <item type="bool" name="config_tether_upstream_automatic"/>
+ <!-- Configuration values for tethering entitlement check -->
+ <item type="array" name="config_mobile_hotspot_provision_app"/>
+ <item type="string" name="config_mobile_hotspot_provision_app_no_ui"/>
+ <item type="string" name="config_mobile_hotspot_provision_response"/>
+ <item type="integer" name="config_mobile_hotspot_provision_check_period"/>
+ <item type="string" name="config_wifi_tether_enable"/>
+ <!-- Params from config.xml that can be overlaid -->
+ </policy>
+ </overlayable>
+</resources>
diff --git a/Tethering/res/values/strings.xml b/Tethering/res/values/strings.xml
new file mode 100644
index 0000000..d63c7c5
--- /dev/null
+++ b/Tethering/res/values/strings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Shown when the device is tethered -->
+ <!-- String for tethered notification title [CHAR LIMIT=200] -->
+ <string name="tethered_notification_title">Tethering or hotspot active</string>
+ <!-- String for tethered notification message [CHAR LIMIT=200] -->
+ <string name="tethered_notification_message">Tap to set up.</string>
+
+ <!-- This notification is shown when tethering has been disabled on a user's device.
+ The device is managed by the user's employer. Tethering can't be turned on unless the
+ IT administrator allows it. The noun "admin" is another reference for "IT administrator." -->
+ <!-- String for tether disabling notification title [CHAR LIMIT=200] -->
+ <string name="disable_tether_notification_title">Tethering is disabled</string>
+ <!-- String for tether disabling notification message [CHAR LIMIT=200] -->
+ <string name="disable_tether_notification_message">Contact your admin for details</string>
+
+ <!-- This string should be consistent with the "Hotspot & tethering" text in the "Network and
+ Internet" settings page. That is currently the tether_settings_title_all string. -->
+ <!-- String for tether notification channel name [CHAR LIMIT=200] -->
+ <string name="notification_channel_tethering_status">Hotspot & tethering status</string>
+
+ <!-- String for no upstream notification title [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_title"></string>
+ <!-- String for no upstream notification message [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_message"></string>
+ <!-- String for no upstream notification disable button [CHAR LIMIT=200] -->
+ <string name="no_upstream_notification_disable_button"></string>
+
+ <!-- String for cellular roaming notification title [CHAR LIMIT=200] -->
+ <string name="upstream_roaming_notification_title"></string>
+ <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
+ <string name="upstream_roaming_notification_message"></string>
+</resources>
diff --git a/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java b/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java
new file mode 100644
index 0000000..9fda125
--- /dev/null
+++ b/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.dhcp;
+
+/**
+ * Convenience wrapper around IDhcpServerCallbacks.Stub that implements getInterfaceVersion().
+ * @hide
+ */
+public abstract class DhcpServerCallbacks extends IDhcpServerCallbacks.Stub {
+ /**
+ * Get the version of the aidl interface implemented by the callbacks.
+ */
+ @Override
+ public int getInterfaceVersion() {
+ return IDhcpServerCallbacks.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return IDhcpServerCallbacks.HASH;
+ }
+}
diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
new file mode 100644
index 0000000..aaaec17
--- /dev/null
+++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.dhcp;
+
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
+
+import android.net.LinkAddress;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.net.Inet4Address;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Subclass of {@link DhcpServingParamsParcel} with additional utility methods for building.
+ *
+ * <p>This utility class does not check for validity of the parameters: invalid parameters are
+ * reported by the receiving module when unparceling the parcel.
+ *
+ * @see DhcpServingParams
+ * @hide
+ */
+public class DhcpServingParamsParcelExt extends DhcpServingParamsParcel {
+ public static final int MTU_UNSET = 0;
+
+ /**
+ * Set the server address and served prefix for the DHCP server.
+ *
+ * <p>This parameter is required.
+ */
+ public DhcpServingParamsParcelExt setServerAddr(@NonNull LinkAddress serverAddr) {
+ this.serverAddr = inet4AddressToIntHTH((Inet4Address) serverAddr.getAddress());
+ this.serverAddrPrefixLength = serverAddr.getPrefixLength();
+ return this;
+ }
+
+ /**
+ * Set the default routers to be advertised to DHCP clients.
+ *
+ * <p>Each router must be inside the served prefix. This may be an empty set, but it must
+ * always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Set<Inet4Address> defaultRouters) {
+ this.defaultRouters = toIntArray(defaultRouters);
+ return this;
+ }
+
+ /**
+ * Set the default routers to be advertised to DHCP clients.
+ *
+ * <p>Each router must be inside the served prefix. This may be an empty list of routers,
+ * but it must always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Inet4Address... defaultRouters) {
+ return setDefaultRouters(newArraySet(defaultRouters));
+ }
+
+ /**
+ * Convenience method to build the parameters with no default router.
+ *
+ * <p>Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address.
+ */
+ public DhcpServingParamsParcelExt setNoDefaultRouter() {
+ return setDefaultRouters();
+ }
+
+ /**
+ * Set the DNS servers to be advertised to DHCP clients.
+ *
+ * <p>This may be an empty set, but it must always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDnsServers(@NonNull Set<Inet4Address> dnsServers) {
+ this.dnsServers = toIntArray(dnsServers);
+ return this;
+ }
+
+ /**
+ * Set the DNS servers to be advertised to DHCP clients.
+ *
+ * <p>This may be an empty list of servers, but it must always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDnsServers(@NonNull Inet4Address... dnsServers) {
+ return setDnsServers(newArraySet(dnsServers));
+ }
+
+ /**
+ * Convenience method to build the parameters with no DNS server.
+ *
+ * <p>Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address.
+ */
+ public DhcpServingParamsParcelExt setNoDnsServer() {
+ return setDnsServers();
+ }
+
+ /**
+ * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+ *
+ * <p>This parameter is optional. DNS servers and default routers are always excluded
+ * and do not need to be set here.
+ */
+ public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Set<Inet4Address> excludedAddrs) {
+ this.excludedAddrs = toIntArray(excludedAddrs);
+ return this;
+ }
+
+ /**
+ * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+ *
+ * <p>This parameter is optional. DNS servers and default routers are always excluded
+ * and do not need to be set here.
+ */
+ public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) {
+ return setExcludedAddrs(newArraySet(excludedAddrs));
+ }
+
+ /**
+ * Set the lease time for leases assigned by the DHCP server.
+ *
+ * <p>This parameter is required.
+ */
+ public DhcpServingParamsParcelExt setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) {
+ this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
+ return this;
+ }
+
+ /**
+ * Set the link MTU to be advertised to DHCP clients.
+ *
+ * <p>If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter
+ * is optional and defaults to {@link #MTU_UNSET}.
+ */
+ public DhcpServingParamsParcelExt setLinkMtu(int linkMtu) {
+ this.linkMtu = linkMtu;
+ return this;
+ }
+
+ /**
+ * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option.
+ *
+ * <p>If not set, the default value is false.
+ */
+ public DhcpServingParamsParcelExt setMetered(boolean metered) {
+ this.metered = metered;
+ return this;
+ }
+
+ /**
+ * Set the client address to tell DHCP server only offer this address.
+ * The client's prefix length is the same as server's.
+ *
+ * <p>If not set, the default value is null.
+ */
+ public DhcpServingParamsParcelExt setSingleClientAddr(@Nullable Inet4Address clientAddr) {
+ this.singleClientAddr = clientAddr == null ? 0 : inet4AddressToIntHTH(clientAddr);
+ return this;
+ }
+
+ /**
+ * Set whether the DHCP server should request a new prefix from IpServer when receiving
+ * DHCPDECLINE message in certain particular link (e.g. there is only one downstream USB
+ * tethering client). If it's false, process DHCPDECLINE message as RFC2131#4.3.3 suggests.
+ *
+ * <p>If not set, the default value is false.
+ */
+ public DhcpServingParamsParcelExt setChangePrefixOnDecline(boolean changePrefixOnDecline) {
+ this.changePrefixOnDecline = changePrefixOnDecline;
+ return this;
+ }
+
+ private static int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
+ int[] res = new int[addrs.size()];
+ int i = 0;
+ for (Inet4Address addr : addrs) {
+ res[i] = inet4AddressToIntHTH(addr);
+ i++;
+ }
+ return res;
+ }
+
+ private static ArraySet<Inet4Address> newArraySet(Inet4Address... addrs) {
+ ArraySet<Inet4Address> addrSet = new ArraySet<>(addrs.length);
+ Collections.addAll(addrSet, addrs);
+ return addrSet;
+ }
+}
diff --git a/Tethering/src/android/net/ip/DadProxy.java b/Tethering/src/android/net/ip/DadProxy.java
new file mode 100644
index 0000000..e2976b7
--- /dev/null
+++ b/Tethering/src/android/net/ip/DadProxy.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.net.ip;
+
+import android.net.util.InterfaceParams;
+import android.os.Handler;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Basic Duplicate address detection proxy.
+ *
+ * @hide
+ */
+public class DadProxy {
+ private static final String TAG = DadProxy.class.getSimpleName();
+
+ @VisibleForTesting
+ public static NeighborPacketForwarder naForwarder;
+ public static NeighborPacketForwarder nsForwarder;
+
+ public DadProxy(Handler h, InterfaceParams tetheredIface) {
+ naForwarder = new NeighborPacketForwarder(h, tetheredIface,
+ NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
+ nsForwarder = new NeighborPacketForwarder(h, tetheredIface,
+ NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
+ }
+
+ /** Stop NS/NA Forwarders. */
+ public void stop() {
+ naForwarder.stop();
+ nsForwarder.stop();
+ }
+
+ /** Set upstream iface on both forwarders. */
+ public void setUpstreamIface(InterfaceParams upstreamIface) {
+ naForwarder.setUpstreamIface(upstreamIface);
+ nsForwarder.setUpstreamIface(upstreamIface);
+ }
+}
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
new file mode 100644
index 0000000..c45ce83
--- /dev/null
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -0,0 +1,1469 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.TetheringRequest.checkStaticAddressConfiguration;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.net.util.NetworkConstants.asByte;
+import static android.net.util.PrefixUtils.asIpPrefix;
+import static android.net.util.TetheringMessageBase.BASE_IPSERVER;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+
+import android.net.INetd;
+import android.net.INetworkStackStatusCallback;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.RouteInfo;
+import android.net.TetheredClient;
+import android.net.TetheringManager;
+import android.net.TetheringRequestParcel;
+import android.net.dhcp.DhcpLeaseParcelable;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.DhcpServingParamsParcelExt;
+import android.net.dhcp.IDhcpEventCallbacks;
+import android.net.dhcp.IDhcpServer;
+import android.net.ip.IpNeighborMonitor.NeighborEvent;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.shared.NetdUtils;
+import android.net.shared.RouteUtils;
+import android.net.util.InterfaceParams;
+import android.net.util.InterfaceSet;
+import android.net.util.PrefixUtils;
+import android.net.util.SharedLog;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.BpfCoordinator;
+import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.PrivateAddressCoordinator;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * Provides the interface to IP-layer serving functionality for a given network
+ * interface, e.g. for tethering or "local-only hotspot" mode.
+ *
+ * @hide
+ */
+public class IpServer extends StateMachine {
+ public static final int STATE_UNAVAILABLE = 0;
+ public static final int STATE_AVAILABLE = 1;
+ public static final int STATE_TETHERED = 2;
+ public static final int STATE_LOCAL_ONLY = 3;
+
+ /** Get string name of |state|.*/
+ public static String getStateString(int state) {
+ switch (state) {
+ case STATE_UNAVAILABLE: return "UNAVAILABLE";
+ case STATE_AVAILABLE: return "AVAILABLE";
+ case STATE_TETHERED: return "TETHERED";
+ case STATE_LOCAL_ONLY: return "LOCAL_ONLY";
+ }
+ return "UNKNOWN: " + state;
+ }
+
+ private static final byte DOUG_ADAMS = (byte) 42;
+
+ // TODO: have PanService use some visible version of this constant
+ private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1/24";
+
+ // TODO: have this configurable
+ private static final int DHCP_LEASE_TIME_SECS = 3600;
+
+ private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString("00:00:00:00:00:00");
+
+ private static final String TAG = "IpServer";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+ private static final Class[] sMessageClasses = {
+ IpServer.class
+ };
+ private static final SparseArray<String> sMagicDecoderRing =
+ MessageUtils.findMessageNames(sMessageClasses);
+
+ /** IpServer callback. */
+ public static class Callback {
+ /**
+ * Notify that |who| has changed its tethering state.
+ *
+ * @param who the calling instance of IpServer
+ * @param state one of STATE_*
+ * @param lastError one of TetheringManager.TETHER_ERROR_*
+ */
+ public void updateInterfaceState(IpServer who, int state, int lastError) { }
+
+ /**
+ * Notify that |who| has new LinkProperties.
+ *
+ * @param who the calling instance of IpServer
+ * @param newLp the new LinkProperties to report
+ */
+ public void updateLinkProperties(IpServer who, LinkProperties newLp) { }
+
+ /**
+ * Notify that the DHCP leases changed in one of the IpServers.
+ */
+ public void dhcpLeasesChanged() { }
+
+ /**
+ * Request Tethering change.
+ *
+ * @param tetheringType the downstream type of this IpServer.
+ * @param enabled enable or disable tethering.
+ */
+ public void requestEnableTethering(int tetheringType, boolean enabled) { }
+ }
+
+ /** Capture IpServer dependencies, for injection. */
+ public abstract static class Dependencies {
+ /**
+ * Create a DadProxy instance to be used by IpServer.
+ * To support multiple tethered interfaces concurrently DAD Proxy
+ * needs to be supported per IpServer instead of per upstream.
+ */
+ public DadProxy getDadProxy(Handler handler, InterfaceParams ifParams) {
+ 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);
+ }
+
+ /** Get |ifName|'s interface information.*/
+ public InterfaceParams getInterfaceParams(String ifName) {
+ return InterfaceParams.getByName(ifName);
+ }
+
+ /** Create a DhcpServer instance to be used by IpServer. */
+ public abstract void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+ DhcpServerCallbacks cb);
+ }
+
+ // request from the user that it wants to tether
+ public static final int CMD_TETHER_REQUESTED = BASE_IPSERVER + 1;
+ // request from the user that it wants to untether
+ public static final int CMD_TETHER_UNREQUESTED = BASE_IPSERVER + 2;
+ // notification that this interface is down
+ public static final int CMD_INTERFACE_DOWN = BASE_IPSERVER + 3;
+ // notification from the {@link Tethering.TetherMainSM} that it had trouble enabling IP
+ // Forwarding
+ public static final int CMD_IP_FORWARDING_ENABLE_ERROR = BASE_IPSERVER + 4;
+ // notification from the {@link Tethering.TetherMainSM} SM that it had trouble disabling IP
+ // Forwarding
+ public static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IPSERVER + 5;
+ // notification from the {@link Tethering.TetherMainSM} SM that it had trouble starting
+ // tethering
+ public static final int CMD_START_TETHERING_ERROR = BASE_IPSERVER + 6;
+ // notification from the {@link Tethering.TetherMainSM} that it had trouble stopping tethering
+ public static final int CMD_STOP_TETHERING_ERROR = BASE_IPSERVER + 7;
+ // notification from the {@link Tethering.TetherMainSM} that it had trouble setting the DNS
+ // forwarders
+ public static final int CMD_SET_DNS_FORWARDERS_ERROR = BASE_IPSERVER + 8;
+ // the upstream connection has changed
+ 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;
+ // request from PrivateAddressCoordinator to restart tethering.
+ public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 13;
+
+ private final State mInitialState;
+ private final State mLocalHotspotState;
+ private final State mTetheredState;
+ private final State mUnavailableState;
+ private final State mWaitingForRestartState;
+
+ private final SharedLog mLog;
+ private final INetd mNetd;
+ @NonNull
+ private final BpfCoordinator mBpfCoordinator;
+ private final Callback mCallback;
+ private final InterfaceController mInterfaceCtrl;
+ private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+
+ private final String mIfaceName;
+ private final int mInterfaceType;
+ private final LinkProperties mLinkProperties;
+ private final boolean mUsingLegacyDhcp;
+ private final boolean mUsingBpfOffload;
+
+ private final Dependencies mDeps;
+
+ private int mLastError;
+ private int mServingMode;
+ private InterfaceSet mUpstreamIfaceSet; // may change over time
+ private InterfaceParams mInterfaceParams;
+ // TODO: De-duplicate this with mLinkProperties above. Currently, these link
+ // properties are those selected by the IPv6TetheringCoordinator and relayed
+ // to us. By comparison, mLinkProperties contains the addresses and directly
+ // connected routes that have been formed from these properties iff. we have
+ // succeeded in configuring them and are able to announce them within Router
+ // Advertisements (otherwise, we do not add them to mLinkProperties at all).
+ private LinkProperties mLastIPv6LinkProperties;
+ private RouterAdvertisementDaemon mRaDaemon;
+ private DadProxy mDadProxy;
+
+ // To be accessed only on the handler thread
+ private int mDhcpServerStartIndex = 0;
+ private IDhcpServer mDhcpServer;
+ private RaParams mLastRaParams;
+
+ private LinkAddress mStaticIpv4ServerAddr;
+ private LinkAddress mStaticIpv4ClientAddr;
+
+ @NonNull
+ private List<TetheredClient> mDhcpLeases = Collections.emptyList();
+
+ private int mLastIPv6UpstreamIfindex = 0;
+
+ private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
+ public void accept(NeighborEvent e) {
+ sendMessage(CMD_NEIGHBOR_EVENT, e);
+ }
+ }
+
+ private final IpNeighborMonitor mIpNeighborMonitor;
+
+ private LinkAddress mIpv4Address;
+
+ // TODO: Add a dependency object to pass the data members or variables from the tethering
+ // object. It helps to reduce the arguments of the constructor.
+ public IpServer(
+ String ifaceName, Looper looper, int interfaceType, SharedLog log,
+ INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
+ boolean usingLegacyDhcp, boolean usingBpfOffload,
+ PrivateAddressCoordinator addressCoordinator, Dependencies deps) {
+ super(ifaceName, looper);
+ mLog = log.forSubComponent(ifaceName);
+ mNetd = netd;
+ mBpfCoordinator = coordinator;
+ mCallback = callback;
+ mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
+ mIfaceName = ifaceName;
+ mInterfaceType = interfaceType;
+ mLinkProperties = new LinkProperties();
+ mUsingLegacyDhcp = usingLegacyDhcp;
+ mUsingBpfOffload = usingBpfOffload;
+ mPrivateAddressCoordinator = addressCoordinator;
+ mDeps = deps;
+ resetLinkProperties();
+ mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
+ mServingMode = STATE_AVAILABLE;
+
+ mIpNeighborMonitor = mDeps.getIpNeighborMonitor(getHandler(), mLog,
+ new MyNeighborEventConsumer());
+
+ // IP neighbor monitor monitors the neighbor events for adding/removing offload
+ // forwarding rules per client. If BPF offload is not supported, don't start listening
+ // for neighbor events. See updateIpv6ForwardingRules, addIpv6ForwardingRule,
+ // removeIpv6ForwardingRule.
+ if (mUsingBpfOffload && !mIpNeighborMonitor.start()) {
+ mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName);
+ }
+
+ mInitialState = new InitialState();
+ mLocalHotspotState = new LocalHotspotState();
+ mTetheredState = new TetheredState();
+ mUnavailableState = new UnavailableState();
+ mWaitingForRestartState = new WaitingForRestartState();
+ addState(mInitialState);
+ addState(mLocalHotspotState);
+ addState(mTetheredState);
+ addState(mWaitingForRestartState, mTetheredState);
+ addState(mUnavailableState);
+
+ setInitialState(mInitialState);
+ }
+
+ /** Interface name which IpServer served.*/
+ public String interfaceName() {
+ return mIfaceName;
+ }
+
+ /**
+ * Tethering downstream type. It would be one of TetheringManager#TETHERING_*.
+ */
+ public int interfaceType() {
+ return mInterfaceType;
+ }
+
+ /** Last error from this IpServer. */
+ public int lastError() {
+ return mLastError;
+ }
+
+ /** Serving mode is the current state of IpServer state machine. */
+ public int servingMode() {
+ return mServingMode;
+ }
+
+ /** The properties of the network link which IpServer is serving. */
+ public LinkProperties linkProperties() {
+ return new LinkProperties(mLinkProperties);
+ }
+
+ /** The address which IpServer is using. */
+ public LinkAddress getAddress() {
+ return mIpv4Address;
+ }
+
+ /**
+ * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
+ * thread.
+ */
+ public List<TetheredClient> getAllLeases() {
+ return Collections.unmodifiableList(mDhcpLeases);
+ }
+
+ /** Stop this IpServer. After this is called this IpServer should not be used any more. */
+ public void stop() {
+ sendMessage(CMD_INTERFACE_DOWN);
+ }
+
+ /**
+ * Tethering is canceled. IpServer state machine will be available and wait for
+ * next tethering request.
+ */
+ public void unwanted() {
+ sendMessage(CMD_TETHER_UNREQUESTED);
+ }
+
+ /** Internals. */
+
+ private boolean startIPv4() {
+ return configureIPv4(true);
+ }
+
+ /**
+ * Convenience wrapper around INetworkStackStatusCallback to run callbacks on the IpServer
+ * handler.
+ *
+ * <p>Different instances of this class can be created for each call to IDhcpServer methods,
+ * with different implementations of the callback, to differentiate handling of success/error in
+ * each call.
+ */
+ private abstract class OnHandlerStatusCallback extends INetworkStackStatusCallback.Stub {
+ @Override
+ public void onStatusAvailable(int statusCode) {
+ getHandler().post(() -> callback(statusCode));
+ }
+
+ public abstract void callback(int statusCode);
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return this.HASH;
+ }
+ }
+
+ private class DhcpServerCallbacksImpl extends DhcpServerCallbacks {
+ private final int mStartIndex;
+
+ private DhcpServerCallbacksImpl(int startIndex) {
+ mStartIndex = startIndex;
+ }
+
+ @Override
+ public void onDhcpServerCreated(int statusCode, IDhcpServer server) throws RemoteException {
+ getHandler().post(() -> {
+ // We are on the handler thread: mDhcpServerStartIndex can be read safely.
+ if (mStartIndex != mDhcpServerStartIndex) {
+ // This start request is obsolete. Explicitly stop the DHCP server to shut
+ // down its thread. When the |server| binder token goes out of scope, the
+ // garbage collector will finalize it, which causes the network stack process
+ // garbage collector to collect the server itself.
+ try {
+ server.stop(null);
+ } catch (RemoteException e) { }
+ return;
+ }
+
+ if (statusCode != STATUS_SUCCESS) {
+ mLog.e("Error obtaining DHCP server: " + statusCode);
+ handleError();
+ return;
+ }
+
+ mDhcpServer = server;
+ try {
+ mDhcpServer.startWithCallbacks(new OnHandlerStatusCallback() {
+ @Override
+ public void callback(int startStatusCode) {
+ if (startStatusCode != STATUS_SUCCESS) {
+ mLog.e("Error starting DHCP server: " + startStatusCode);
+ handleError();
+ }
+ }
+ }, new DhcpEventCallback());
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ }
+
+ private void handleError() {
+ mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+ transitionTo(mInitialState);
+ }
+ }
+
+ private class DhcpEventCallback extends IDhcpEventCallbacks.Stub {
+ @Override
+ public void onLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables) {
+ final ArrayList<TetheredClient> leases = new ArrayList<>();
+ for (DhcpLeaseParcelable lease : leaseParcelables) {
+ final LinkAddress address = new LinkAddress(
+ intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
+ 0 /* flags */, RT_SCOPE_UNIVERSE /* as per RFC6724#3.2 */,
+ lease.expTime /* deprecationTime */, lease.expTime /* expirationTime */);
+
+ final MacAddress macAddress;
+ try {
+ macAddress = MacAddress.fromBytes(lease.hwAddr);
+ } catch (IllegalArgumentException e) {
+ Log.wtf(TAG, "Invalid address received from DhcpServer: "
+ + Arrays.toString(lease.hwAddr));
+ return;
+ }
+
+ final TetheredClient.AddressInfo addressInfo = new TetheredClient.AddressInfo(
+ address, lease.hostname);
+ leases.add(new TetheredClient(
+ macAddress,
+ Collections.singletonList(addressInfo),
+ mInterfaceType));
+ }
+
+ getHandler().post(() -> {
+ mDhcpLeases = leases;
+ mCallback.dhcpLeasesChanged();
+ });
+ }
+
+ @Override
+ public void onNewPrefixRequest(@NonNull final IpPrefix currentPrefix) {
+ Objects.requireNonNull(currentPrefix);
+ sendMessage(CMD_NEW_PREFIX_REQUEST, currentPrefix);
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() throws RemoteException {
+ return this.HASH;
+ }
+ }
+
+ private RouteInfo getDirectConnectedRoute(@NonNull final LinkAddress ipv4Address) {
+ Objects.requireNonNull(ipv4Address);
+ return new RouteInfo(PrefixUtils.asIpPrefix(ipv4Address), null, mIfaceName, RTN_UNICAST);
+ }
+
+ private DhcpServingParamsParcel makeServingParams(@NonNull final Inet4Address defaultRouter,
+ @NonNull final Inet4Address dnsServer, @NonNull LinkAddress serverAddr,
+ @Nullable Inet4Address clientAddr) {
+ final boolean changePrefixOnDecline =
+ (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null);
+ return new DhcpServingParamsParcelExt()
+ .setDefaultRouters(defaultRouter)
+ .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS)
+ .setDnsServers(dnsServer)
+ .setServerAddr(serverAddr)
+ .setMetered(true)
+ .setSingleClientAddr(clientAddr)
+ .setChangePrefixOnDecline(changePrefixOnDecline);
+ // TODO: also advertise link MTU
+ }
+
+ private boolean startDhcp(final LinkAddress serverLinkAddr, final LinkAddress clientLinkAddr) {
+ if (mUsingLegacyDhcp) {
+ return true;
+ }
+
+ final Inet4Address addr = (Inet4Address) serverLinkAddr.getAddress();
+ final Inet4Address clientAddr = clientLinkAddr == null ? null :
+ (Inet4Address) clientLinkAddr.getAddress();
+
+ final DhcpServingParamsParcel params = makeServingParams(addr /* defaultRouter */,
+ addr /* dnsServer */, serverLinkAddr, clientAddr);
+ mDhcpServerStartIndex++;
+ mDeps.makeDhcpServer(
+ mIfaceName, params, new DhcpServerCallbacksImpl(mDhcpServerStartIndex));
+ return true;
+ }
+
+ private void stopDhcp() {
+ // Make all previous start requests obsolete so servers are not started later
+ mDhcpServerStartIndex++;
+
+ if (mDhcpServer != null) {
+ try {
+ mDhcpServer.stop(new OnHandlerStatusCallback() {
+ @Override
+ public void callback(int statusCode) {
+ if (statusCode != STATUS_SUCCESS) {
+ mLog.e("Error stopping DHCP server: " + statusCode);
+ mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+ // Not much more we can do here
+ }
+ mDhcpLeases.clear();
+ getHandler().post(mCallback::dhcpLeasesChanged);
+ }
+ });
+ mDhcpServer = null;
+ } catch (RemoteException e) {
+ mLog.e("Error stopping DHCP server", e);
+ // Not much more we can do here
+ }
+ }
+ }
+
+ private boolean configureDhcp(boolean enable, final LinkAddress serverAddr,
+ final LinkAddress clientAddr) {
+ if (enable) {
+ return startDhcp(serverAddr, clientAddr);
+ } else {
+ stopDhcp();
+ return true;
+ }
+ }
+
+ private void stopIPv4() {
+ configureIPv4(false);
+ // NOTE: All of configureIPv4() will be refactored out of existence
+ // into calls to InterfaceController, shared with startIPv4().
+ mInterfaceCtrl.clearIPv4Address();
+ mPrivateAddressCoordinator.releaseDownstream(this);
+ mIpv4Address = null;
+ mStaticIpv4ServerAddr = null;
+ mStaticIpv4ClientAddr = null;
+ }
+
+ private boolean configureIPv4(boolean enabled) {
+ if (VDBG) Log.d(TAG, "configureIPv4(" + enabled + ")");
+
+ if (enabled) {
+ mIpv4Address = requestIpv4Address(true /* useLastAddress */);
+ }
+
+ if (mIpv4Address == null) {
+ mLog.e("No available ipv4 address");
+ return false;
+ }
+
+ if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) {
+ // BT configures the interface elsewhere: only start DHCP.
+ // TODO: make all tethering types behave the same way, and delete the bluetooth
+ // code that calls into NetworkManagementService directly.
+ return configureDhcp(enabled, mIpv4Address, null /* clientAddress */);
+ }
+
+ final IpPrefix ipv4Prefix = asIpPrefix(mIpv4Address);
+
+ final Boolean setIfaceUp;
+ if (mInterfaceType == TetheringManager.TETHERING_WIFI
+ || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
+ || mInterfaceType == TetheringManager.TETHERING_ETHERNET
+ || mInterfaceType == TetheringManager.TETHERING_WIGIG) {
+ // The WiFi and Ethernet stack has ownership of the interface up/down state.
+ // It is unclear whether the Bluetooth or USB stacks will manage their own
+ // state.
+ setIfaceUp = null;
+ } else {
+ setIfaceUp = enabled;
+ }
+ if (!mInterfaceCtrl.setInterfaceConfiguration(mIpv4Address, setIfaceUp)) {
+ mLog.e("Error configuring interface");
+ if (!enabled) stopDhcp();
+ return false;
+ }
+
+ if (enabled) {
+ mLinkProperties.addLinkAddress(mIpv4Address);
+ mLinkProperties.addRoute(getDirectConnectedRoute(mIpv4Address));
+ } else {
+ mLinkProperties.removeLinkAddress(mIpv4Address);
+ mLinkProperties.removeRoute(getDirectConnectedRoute(mIpv4Address));
+ }
+ return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr);
+ }
+
+ private LinkAddress requestIpv4Address(final boolean useLastAddress) {
+ if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
+
+ if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) {
+ return new LinkAddress(BLUETOOTH_IFACE_ADDR);
+ }
+
+ return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress);
+ }
+
+ private boolean startIPv6() {
+ mInterfaceParams = mDeps.getInterfaceParams(mIfaceName);
+ if (mInterfaceParams == null) {
+ mLog.e("Failed to find InterfaceParams");
+ stopIPv6();
+ return false;
+ }
+
+ mRaDaemon = mDeps.getRouterAdvertisementDaemon(mInterfaceParams);
+ if (!mRaDaemon.start()) {
+ stopIPv6();
+ return false;
+ }
+
+ // TODO: use ShimUtils instead of explicitly checking the version here.
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R || "S".equals(Build.VERSION.CODENAME)
+ || "T".equals(Build.VERSION.CODENAME)) {
+ // DAD Proxy starts forwarding packets after IPv6 upstream is present.
+ mDadProxy = mDeps.getDadProxy(getHandler(), mInterfaceParams);
+ }
+
+ return true;
+ }
+
+ private void stopIPv6() {
+ mInterfaceParams = null;
+ setRaParams(null);
+
+ if (mRaDaemon != null) {
+ mRaDaemon.stop();
+ mRaDaemon = null;
+ }
+
+ if (mDadProxy != null) {
+ mDadProxy.stop();
+ mDadProxy = null;
+ }
+ }
+
+ // IPv6TetheringCoordinator sends updates with carefully curated IPv6-only
+ // LinkProperties. These have extraneous data filtered out and only the
+ // necessary prefixes included (per its prefix distribution policy).
+ //
+ // TODO: Evaluate using a data structure than is more directly suited to
+ // communicating only the relevant information.
+ private void updateUpstreamIPv6LinkProperties(LinkProperties v6only, int ttlAdjustment) {
+ if (mRaDaemon == null) return;
+
+ // Avoid unnecessary work on spurious updates.
+ if (Objects.equals(mLastIPv6LinkProperties, v6only)) {
+ return;
+ }
+
+ RaParams params = null;
+ String upstreamIface = null;
+ InterfaceParams upstreamIfaceParams = null;
+ int upstreamIfIndex = 0;
+
+ if (v6only != null) {
+ upstreamIface = v6only.getInterfaceName();
+ upstreamIfaceParams = mDeps.getInterfaceParams(upstreamIface);
+ if (upstreamIfaceParams != null) {
+ upstreamIfIndex = upstreamIfaceParams.index;
+ }
+ params = new RaParams();
+ params.mtu = v6only.getMtu();
+ params.hasDefaultRoute = v6only.hasIpv6DefaultRoute();
+
+ if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment);
+
+ for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
+ if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+
+ final IpPrefix prefix = new IpPrefix(
+ linkAddr.getAddress(), linkAddr.getPrefixLength());
+ params.prefixes.add(prefix);
+
+ final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
+ if (dnsServer != null) {
+ params.dnses.add(dnsServer);
+ }
+ }
+ }
+
+ // Add upstream index to name mapping. See the comment of the interface mapping update in
+ // CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential
+ // timing issue. It prevents that the IPv6 capability is updated later than
+ // CMD_TETHER_CONNECTION_CHANGED.
+ mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
+
+ // If v6only is null, we pass in null to setRaParams(), which handles
+ // deprecation of any existing RA data.
+
+ setRaParams(params);
+ mLastIPv6LinkProperties = v6only;
+
+ updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, null);
+ mLastIPv6UpstreamIfindex = upstreamIfIndex;
+ if (mDadProxy != null) {
+ mDadProxy.setUpstreamIface(upstreamIfaceParams);
+ }
+ }
+
+ private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {
+ final int removalFailures = RouteUtils.removeRoutesFromLocalNetwork(
+ mNetd, toBeRemoved);
+ if (removalFailures > 0) {
+ mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
+ removalFailures));
+ }
+
+ for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
+ }
+
+ private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ try {
+ // It's safe to call networkAddInterface() even if
+ // the interface is already in the local_network.
+ mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName);
+ try {
+ // Add routes from local network. Note that adding routes that
+ // already exist does not cause an error (EEXIST is silently ignored).
+ RouteUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+ } catch (IllegalStateException e) {
+ mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
+ return;
+ }
+ } catch (ServiceSpecificException | RemoteException e) {
+ mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
+ return;
+ }
+
+ for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route);
+ }
+
+ private void configureLocalIPv6Routes(
+ HashSet<IpPrefix> deprecatedPrefixes, HashSet<IpPrefix> newPrefixes) {
+ // [1] Remove the routes that are deprecated.
+ if (!deprecatedPrefixes.isEmpty()) {
+ removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
+ }
+
+ // [2] Add only the routes that have not previously been added.
+ if (newPrefixes != null && !newPrefixes.isEmpty()) {
+ HashSet<IpPrefix> addedPrefixes = (HashSet) newPrefixes.clone();
+ if (mLastRaParams != null) {
+ addedPrefixes.removeAll(mLastRaParams.prefixes);
+ }
+
+ if (!addedPrefixes.isEmpty()) {
+ addRoutesToLocalNetwork(getLocalRoutesFor(mIfaceName, addedPrefixes));
+ }
+ }
+ }
+
+ private void configureLocalIPv6Dns(
+ HashSet<Inet6Address> deprecatedDnses, HashSet<Inet6Address> newDnses) {
+ // TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located?
+ if (mNetd == null) {
+ if (newDnses != null) newDnses.clear();
+ mLog.e("No netd service instance available; not setting local IPv6 addresses");
+ return;
+ }
+
+ // [1] Remove deprecated local DNS IP addresses.
+ if (!deprecatedDnses.isEmpty()) {
+ for (Inet6Address dns : deprecatedDnses) {
+ if (!mInterfaceCtrl.removeAddress(dns, RFC7421_PREFIX_LENGTH)) {
+ mLog.e("Failed to remove local dns IP " + dns);
+ }
+
+ mLinkProperties.removeLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH));
+ }
+ }
+
+ // [2] Add only the local DNS IP addresses that have not previously been added.
+ if (newDnses != null && !newDnses.isEmpty()) {
+ final HashSet<Inet6Address> addedDnses = (HashSet) newDnses.clone();
+ if (mLastRaParams != null) {
+ addedDnses.removeAll(mLastRaParams.dnses);
+ }
+
+ for (Inet6Address dns : addedDnses) {
+ if (!mInterfaceCtrl.addAddress(dns, RFC7421_PREFIX_LENGTH)) {
+ mLog.e("Failed to add local dns IP " + dns);
+ newDnses.remove(dns);
+ }
+
+ mLinkProperties.addLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH));
+ }
+ }
+
+ try {
+ mNetd.tetherApplyDnsInterfaces();
+ } catch (ServiceSpecificException | RemoteException e) {
+ mLog.e("Failed to update local DNS caching server");
+ if (newDnses != null) newDnses.clear();
+ }
+ }
+
+ private void addIpv6ForwardingRule(Ipv6ForwardingRule rule) {
+ // Theoretically, we don't need this check because IP neighbor monitor doesn't start if BPF
+ // offload is disabled. Add this check just in case.
+ // TODO: Perhaps remove this protection check.
+ if (!mUsingBpfOffload) return;
+
+ mBpfCoordinator.tetherOffloadRuleAdd(this, rule);
+ }
+
+ private void removeIpv6ForwardingRule(Ipv6ForwardingRule rule) {
+ // TODO: Perhaps remove this protection check.
+ // See the related comment in #addIpv6ForwardingRule.
+ if (!mUsingBpfOffload) return;
+
+ mBpfCoordinator.tetherOffloadRuleRemove(this, rule);
+ }
+
+ private void clearIpv6ForwardingRules() {
+ if (!mUsingBpfOffload) return;
+
+ mBpfCoordinator.tetherOffloadRuleClear(this);
+ }
+
+ private void updateIpv6ForwardingRule(int newIfindex) {
+ // TODO: Perhaps remove this protection check.
+ // See the related comment in #addIpv6ForwardingRule.
+ if (!mUsingBpfOffload) return;
+
+ mBpfCoordinator.tetherOffloadRuleUpdate(this, newIfindex);
+ }
+
+ // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream
+ // changes or if a neighbor event is received.
+ private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
+ NeighborEvent e) {
+ // If we no longer have an upstream, clear forwarding rules and do nothing else.
+ if (upstreamIfindex == 0) {
+ clearIpv6ForwardingRules();
+ return;
+ }
+
+ // If the upstream interface has changed, remove all rules and re-add them with the new
+ // upstream interface.
+ if (prevUpstreamIfindex != upstreamIfindex) {
+ updateIpv6ForwardingRule(upstreamIfindex);
+ }
+
+ // If we're here to process a NeighborEvent, do so now.
+ // 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 Ipv6ForwardingRule constructor to ensure that we never
+ // add rules with a null MAC, only delete them.
+ MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+ Ipv6ForwardingRule rule = new Ipv6ForwardingRule(upstreamIfindex,
+ mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
+ if (e.isValid()) {
+ addIpv6ForwardingRule(rule);
+ } else {
+ removeIpv6ForwardingRule(rule);
+ }
+ }
+
+ // TODO: consider moving into BpfCoordinator.
+ private void updateClientInfoIpv4(NeighborEvent e) {
+ // TODO: Perhaps remove this protection check.
+ // See the related comment in #addIpv6ForwardingRule.
+ if (!mUsingBpfOffload) return;
+
+ 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 {
+ // TODO: Delete all related offload rules which are using this client.
+ mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
+ }
+ }
+
+ private void handleNeighborEvent(NeighborEvent e) {
+ if (mInterfaceParams != null
+ && mInterfaceParams.index == e.ifindex
+ && mInterfaceParams.hasMacAddress) {
+ updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex, e);
+ updateClientInfoIpv4(e);
+ }
+ }
+
+ private void handleNewPrefixRequest(@NonNull final IpPrefix currentPrefix) {
+ if (!currentPrefix.contains(mIpv4Address.getAddress())
+ || currentPrefix.getPrefixLength() != mIpv4Address.getPrefixLength()) {
+ Log.e(TAG, "Invalid prefix: " + currentPrefix);
+ return;
+ }
+
+ final LinkAddress deprecatedLinkAddress = mIpv4Address;
+ mIpv4Address = requestIpv4Address(false);
+ if (mIpv4Address == null) {
+ mLog.e("Fail to request a new downstream prefix");
+ return;
+ }
+ final Inet4Address srvAddr = (Inet4Address) mIpv4Address.getAddress();
+
+ // Add new IPv4 address on the interface.
+ if (!mInterfaceCtrl.addAddress(srvAddr, currentPrefix.getPrefixLength())) {
+ mLog.e("Failed to add new IP " + srvAddr);
+ return;
+ }
+
+ // Remove deprecated routes from local network.
+ removeRoutesFromLocalNetwork(
+ Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress)));
+ mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
+
+ // Add new routes to local network.
+ addRoutesToLocalNetwork(
+ Collections.singletonList(getDirectConnectedRoute(mIpv4Address)));
+ mLinkProperties.addLinkAddress(mIpv4Address);
+
+ // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
+ // listen on the interface configured with new IPv4 address, that results DNS validation
+ // failure of downstream client even if appropriate routes have been configured.
+ try {
+ mNetd.tetherApplyDnsInterfaces();
+ } catch (ServiceSpecificException | RemoteException e) {
+ mLog.e("Failed to update local DNS caching server");
+ return;
+ }
+ sendLinkProperties();
+
+ // Notify DHCP server that new prefix/route has been applied on IpServer.
+ final Inet4Address clientAddr = mStaticIpv4ClientAddr == null ? null :
+ (Inet4Address) mStaticIpv4ClientAddr.getAddress();
+ final DhcpServingParamsParcel params = makeServingParams(srvAddr /* defaultRouter */,
+ srvAddr /* dnsServer */, mIpv4Address /* serverLinkAddress */, clientAddr);
+ try {
+ mDhcpServer.updateParams(params, new OnHandlerStatusCallback() {
+ @Override
+ public void callback(int statusCode) {
+ if (statusCode != STATUS_SUCCESS) {
+ mLog.e("Error updating DHCP serving params: " + statusCode);
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ mLog.e("Error updating DHCP serving params", e);
+ }
+ }
+
+ private byte getHopLimit(String upstreamIface, int adjustTTL) {
+ try {
+ int upstreamHopLimit = Integer.parseUnsignedInt(
+ mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, upstreamIface, "hop_limit"));
+ upstreamHopLimit = upstreamHopLimit + adjustTTL;
+ // Cap the hop limit to 255.
+ return (byte) Integer.min(upstreamHopLimit, 255);
+ } catch (Exception e) {
+ mLog.e("Failed to find upstream interface hop limit", e);
+ }
+ return RaParams.DEFAULT_HOPLIMIT;
+ }
+
+ private void setRaParams(RaParams newParams) {
+ if (mRaDaemon != null) {
+ final RaParams deprecatedParams =
+ RaParams.getDeprecatedRaParams(mLastRaParams, newParams);
+
+ configureLocalIPv6Routes(deprecatedParams.prefixes,
+ (newParams != null) ? newParams.prefixes : null);
+
+ configureLocalIPv6Dns(deprecatedParams.dnses,
+ (newParams != null) ? newParams.dnses : null);
+
+ mRaDaemon.buildNewRa(deprecatedParams, newParams);
+ }
+
+ mLastRaParams = newParams;
+ }
+
+ private void logMessage(State state, int what) {
+ mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+ }
+
+ private void sendInterfaceState(int newInterfaceState) {
+ mServingMode = newInterfaceState;
+ mCallback.updateInterfaceState(this, newInterfaceState, mLastError);
+ sendLinkProperties();
+ }
+
+ private void sendLinkProperties() {
+ mCallback.updateLinkProperties(this, new LinkProperties(mLinkProperties));
+ }
+
+ private void resetLinkProperties() {
+ mLinkProperties.clear();
+ mLinkProperties.setInterfaceName(mIfaceName);
+ }
+
+ private void maybeConfigureStaticIp(final TetheringRequestParcel 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)) {
+ return;
+ }
+
+ mStaticIpv4ServerAddr = request.localIPv4Address;
+ mStaticIpv4ClientAddr = request.staticClientAddress;
+ }
+
+ class InitialState extends State {
+ @Override
+ public void enter() {
+ sendInterfaceState(STATE_AVAILABLE);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
+ switch (message.arg1) {
+ case STATE_LOCAL_ONLY:
+ maybeConfigureStaticIp((TetheringRequestParcel) message.obj);
+ transitionTo(mLocalHotspotState);
+ break;
+ case STATE_TETHERED:
+ maybeConfigureStaticIp((TetheringRequestParcel) message.obj);
+ transitionTo(mTetheredState);
+ break;
+ default:
+ mLog.e("Invalid tethering interface serving state specified.");
+ }
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ break;
+ case CMD_IPV6_TETHER_UPDATE:
+ updateUpstreamIPv6LinkProperties((LinkProperties) message.obj, message.arg1);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+ }
+
+ private void startConntrackMonitoring() {
+ mBpfCoordinator.startMonitoring(this);
+ }
+
+ private void stopConntrackMonitoring() {
+ mBpfCoordinator.stopMonitoring(this);
+ }
+
+ class BaseServingState extends State {
+ @Override
+ public void enter() {
+ startConntrackMonitoring();
+
+ if (!startIPv4()) {
+ mLastError = TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+ return;
+ }
+
+ try {
+ NetdUtils.tetherInterface(mNetd, mIfaceName, asIpPrefix(mIpv4Address));
+ } catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
+ mLog.e("Error Tethering", e);
+ mLastError = TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+ return;
+ }
+
+ if (!startIPv6()) {
+ mLog.e("Failed to startIPv6");
+ // TODO: Make this a fatal error once Bluetooth IPv6 is sorted.
+ return;
+ }
+ }
+
+ @Override
+ public void exit() {
+ // Note that at this point, we're leaving the tethered state. We can fail any
+ // of these operations, but it doesn't really change that we have to try them
+ // all in sequence.
+ stopIPv6();
+
+ try {
+ NetdUtils.untetherInterface(mNetd, mIfaceName);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLastError = TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+ mLog.e("Failed to untether interface: " + e);
+ }
+
+ stopIPv4();
+ stopConntrackMonitoring();
+
+ resetLinkProperties();
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_UNREQUESTED:
+ transitionTo(mInitialState);
+ if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
+ break;
+ case CMD_IPV6_TETHER_UPDATE:
+ updateUpstreamIPv6LinkProperties((LinkProperties) message.obj, message.arg1);
+ sendLinkProperties();
+ break;
+ case CMD_IP_FORWARDING_ENABLE_ERROR:
+ case CMD_IP_FORWARDING_DISABLE_ERROR:
+ case CMD_START_TETHERING_ERROR:
+ case CMD_STOP_TETHERING_ERROR:
+ case CMD_SET_DNS_FORWARDERS_ERROR:
+ mLastError = TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+ transitionTo(mInitialState);
+ break;
+ case CMD_NEW_PREFIX_REQUEST:
+ handleNewPrefixRequest((IpPrefix) message.obj);
+ break;
+ case CMD_NOTIFY_PREFIX_CONFLICT:
+ mLog.i("restart tethering: " + mInterfaceType);
+ mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
+ transitionTo(mWaitingForRestartState);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Handling errors in BaseServingState.enter() by transitioning is
+ // problematic because transitioning during a multi-state jump yields
+ // a Log.wtf(). Ultimately, there should be only one ServingState,
+ // and forwarding and NAT rules should be handled by a coordinating
+ // functional element outside of IpServer.
+ class LocalHotspotState extends BaseServingState {
+ @Override
+ public void enter() {
+ super.enter();
+ if (mLastError != TetheringManager.TETHER_ERROR_NO_ERROR) {
+ transitionTo(mInitialState);
+ }
+
+ if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
+ sendInterfaceState(STATE_LOCAL_ONLY);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ if (super.processMessage(message)) return true;
+
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLog.e("CMD_TETHER_REQUESTED while in local-only hotspot mode.");
+ break;
+ case CMD_TETHER_CONNECTION_CHANGED:
+ // Ignored in local hotspot state.
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Handling errors in BaseServingState.enter() by transitioning is
+ // problematic because transitioning during a multi-state jump yields
+ // a Log.wtf(). Ultimately, there should be only one ServingState,
+ // and forwarding and NAT rules should be handled by a coordinating
+ // functional element outside of IpServer.
+ class TetheredState extends BaseServingState {
+ @Override
+ public void enter() {
+ super.enter();
+ if (mLastError != TetheringManager.TETHER_ERROR_NO_ERROR) {
+ transitionTo(mInitialState);
+ }
+
+ if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
+ sendInterfaceState(STATE_TETHERED);
+ }
+
+ @Override
+ public void exit() {
+ cleanupUpstream();
+ super.exit();
+ }
+
+ private void cleanupUpstream() {
+ if (mUpstreamIfaceSet == null) return;
+
+ for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
+ mUpstreamIfaceSet = null;
+ clearIpv6ForwardingRules();
+ }
+
+ private void cleanupUpstreamInterface(String upstreamIface) {
+ // Note that we don't care about errors here.
+ // Sometimes interfaces are gone before we get
+ // to remove their rules, which generates errors.
+ // Just do the best we can.
+ mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface);
+ try {
+ mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception in ipfwdRemoveInterfaceForward: " + e.toString());
+ }
+ try {
+ mNetd.tetherRemoveForward(mIfaceName, upstreamIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception in disableNat: " + e.toString());
+ }
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ if (super.processMessage(message)) return true;
+
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLog.e("CMD_TETHER_REQUESTED while already tethering.");
+ break;
+ case CMD_TETHER_CONNECTION_CHANGED:
+ final InterfaceSet newUpstreamIfaceSet = (InterfaceSet) message.obj;
+ if (noChangeInUpstreamIfaceSet(newUpstreamIfaceSet)) {
+ if (VDBG) Log.d(TAG, "Connection changed noop - dropping");
+ break;
+ }
+
+ if (newUpstreamIfaceSet == null) {
+ cleanupUpstream();
+ break;
+ }
+
+ for (String removed : upstreamInterfacesRemoved(newUpstreamIfaceSet)) {
+ cleanupUpstreamInterface(removed);
+ }
+
+ final Set<String> added = upstreamInterfacesAdd(newUpstreamIfaceSet);
+ // This makes the call to cleanupUpstream() in the error
+ // path for any interface neatly cleanup all the interfaces.
+ mUpstreamIfaceSet = newUpstreamIfaceSet;
+
+ for (String ifname : added) {
+ // Add upstream index to name mapping for the tether stats usage in the
+ // coordinator. Although this mapping could be added by both class
+ // Tethering and IpServer, adding mapping from IpServer guarantees that
+ // the mapping is added before adding forwarding rules. That is because
+ // there are different state machines in both classes. It is hard to
+ // guarantee the link property update order between multiple state machines.
+ // Note that both IPv4 and IPv6 interface may be added because
+ // Tethering::setUpstreamNetwork calls getTetheringInterfaces which merges
+ // IPv4 and IPv6 interface name (if any) into an InterfaceSet. The IPv6
+ // capability may be updated later. In that case, IPv6 interface mapping is
+ // updated in updateUpstreamIPv6LinkProperties.
+ if (!ifname.startsWith("v4-")) { // ignore clat interfaces
+ final InterfaceParams upstreamIfaceParams =
+ mDeps.getInterfaceParams(ifname);
+ if (upstreamIfaceParams != null) {
+ mBpfCoordinator.addUpstreamNameToLookupTable(
+ upstreamIfaceParams.index, ifname);
+ }
+ }
+
+ mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
+ try {
+ mNetd.tetherAddForward(mIfaceName, ifname);
+ mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception enabling NAT: " + e.toString());
+ cleanupUpstream();
+ mLastError = TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+ transitionTo(mInitialState);
+ return true;
+ }
+ }
+ break;
+ case CMD_NEIGHBOR_EVENT:
+ handleNeighborEvent((NeighborEvent) message.obj);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private boolean noChangeInUpstreamIfaceSet(InterfaceSet newIfaces) {
+ if (mUpstreamIfaceSet == null && newIfaces == null) return true;
+ if (mUpstreamIfaceSet != null && newIfaces != null) {
+ return mUpstreamIfaceSet.equals(newIfaces);
+ }
+ return false;
+ }
+
+ private Set<String> upstreamInterfacesRemoved(InterfaceSet newIfaces) {
+ if (mUpstreamIfaceSet == null) return new HashSet<>();
+
+ final HashSet<String> removed = new HashSet<>(mUpstreamIfaceSet.ifnames);
+ removed.removeAll(newIfaces.ifnames);
+ return removed;
+ }
+
+ private Set<String> upstreamInterfacesAdd(InterfaceSet newIfaces) {
+ final HashSet<String> added = new HashSet<>(newIfaces.ifnames);
+ if (mUpstreamIfaceSet != null) added.removeAll(mUpstreamIfaceSet.ifnames);
+ return added;
+ }
+ }
+
+ /**
+ * This state is terminal for the per interface state machine. At this
+ * point, the tethering main state machine should have removed this interface
+ * specific state machine from its list of possible recipients of
+ * tethering requests. The state machine itself will hang around until
+ * the garbage collector finds it.
+ */
+ class UnavailableState extends State {
+ @Override
+ public void enter() {
+ mIpNeighborMonitor.stop();
+ mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
+ sendInterfaceState(STATE_UNAVAILABLE);
+ }
+ }
+
+ class WaitingForRestartState extends State {
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_UNREQUESTED:
+ transitionTo(mInitialState);
+ mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ mLog.i("Untethered (interface down) and restarting " + mIfaceName);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Accumulate routes representing "prefixes to be assigned to the local
+ // interface", for subsequent modification of local_network routing.
+ private static ArrayList<RouteInfo> getLocalRoutesFor(
+ String ifname, HashSet<IpPrefix> prefixes) {
+ final ArrayList<RouteInfo> localRoutes = new ArrayList<RouteInfo>();
+ for (IpPrefix ipp : prefixes) {
+ localRoutes.add(new RouteInfo(ipp, null, ifname, RTN_UNICAST));
+ }
+ return localRoutes;
+ }
+
+ // Given a prefix like 2001:db8::/64 return an address like 2001:db8::1.
+ private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) {
+ final byte[] dnsBytes = localPrefix.getRawAddress();
+ dnsBytes[dnsBytes.length - 1] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1));
+ try {
+ return Inet6Address.getByAddress(null, dnsBytes, 0);
+ } catch (UnknownHostException e) {
+ Log.wtf(TAG, "Failed to construct Inet6Address from: " + localPrefix);
+ return null;
+ }
+ }
+
+ private static byte getRandomSanitizedByte(byte dflt, byte... excluded) {
+ final byte random = (byte) (new Random()).nextInt();
+ for (int value : excluded) {
+ if (random == value) return dflt;
+ }
+ return random;
+ }
+}
diff --git a/Tethering/src/android/net/ip/NeighborPacketForwarder.java b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
new file mode 100644
index 0000000..084743d
--- /dev/null
+++ b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
@@ -0,0 +1,181 @@
+/*
+ * 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 android.net.ip;
+
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_RAW;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
+
+import android.net.util.InterfaceParams;
+import android.net.util.SocketUtils;
+import android.net.util.TetheringUtils;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/**
+ * Basic IPv6 Neighbor Advertisement Forwarder.
+ *
+ * Forward NA packets from upstream iface to tethered iface
+ * and NS packets from tethered iface to upstream iface.
+ *
+ * @hide
+ */
+public class NeighborPacketForwarder extends PacketReader {
+ private final String mTag;
+
+ private FileDescriptor mFd;
+
+ // TODO: get these from NetworkStackConstants.
+ private static final int IPV6_ADDR_LEN = 16;
+ private static final int IPV6_DST_ADDR_OFFSET = 24;
+ private static final int IPV6_HEADER_LEN = 40;
+ private static final int ETH_HEADER_LEN = 14;
+
+ private InterfaceParams mListenIfaceParams, mSendIfaceParams;
+
+ private final int mType;
+ public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
+ public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135;
+
+ public NeighborPacketForwarder(Handler h, InterfaceParams tetheredInterface, int type) {
+ super(h);
+ mTag = NeighborPacketForwarder.class.getSimpleName() + "-"
+ + tetheredInterface.name + "-" + type;
+ mType = type;
+
+ if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+ mSendIfaceParams = tetheredInterface;
+ } else {
+ mListenIfaceParams = tetheredInterface;
+ }
+ }
+
+ /** Set new upstream iface and start/stop based on new params. */
+ public void setUpstreamIface(InterfaceParams upstreamParams) {
+ final InterfaceParams oldUpstreamParams;
+
+ if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+ oldUpstreamParams = mListenIfaceParams;
+ mListenIfaceParams = upstreamParams;
+ } else {
+ oldUpstreamParams = mSendIfaceParams;
+ mSendIfaceParams = upstreamParams;
+ }
+
+ if (oldUpstreamParams == null && upstreamParams != null) {
+ start();
+ } else if (oldUpstreamParams != null && upstreamParams == null) {
+ stop();
+ } else if (oldUpstreamParams != null && upstreamParams != null
+ && oldUpstreamParams.index != upstreamParams.index) {
+ stop();
+ start();
+ }
+ }
+
+ // TODO: move NetworkStackUtils.closeSocketQuietly to
+ // frameworks/libs/net/common/device/com/android/net/module/util/[someclass].
+ private void closeSocketQuietly(FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ignored) {
+ }
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ try {
+ // ICMPv6 packets from modem do not have eth header, so RAW socket cannot be used.
+ // To keep uniformity in both directions PACKET socket can be used.
+ mFd = Os.socket(AF_PACKET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
+
+ // TODO: convert setup*Socket to setupICMPv6BpfFilter with filter type?
+ if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+ TetheringUtils.setupNaSocket(mFd);
+ } else if (mType == ICMPV6_NEIGHBOR_SOLICITATION) {
+ TetheringUtils.setupNsSocket(mFd);
+ }
+
+ SocketAddress bindAddress = SocketUtils.makePacketSocketAddress(
+ ETH_P_IPV6, mListenIfaceParams.index);
+ Os.bind(mFd, bindAddress);
+ } catch (ErrnoException | SocketException e) {
+ Log.wtf(mTag, "Failed to create socket", e);
+ closeSocketQuietly(mFd);
+ return null;
+ }
+
+ return mFd;
+ }
+
+ private Inet6Address getIpv6DestinationAddress(byte[] recvbuf) {
+ Inet6Address dstAddr;
+ try {
+ dstAddr = (Inet6Address) Inet6Address.getByAddress(Arrays.copyOfRange(recvbuf,
+ IPV6_DST_ADDR_OFFSET, IPV6_DST_ADDR_OFFSET + IPV6_ADDR_LEN));
+ } catch (UnknownHostException | ClassCastException impossible) {
+ throw new AssertionError("16-byte array not valid IPv6 address?");
+ }
+ return dstAddr;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ if (mSendIfaceParams == null) {
+ return;
+ }
+
+ // The BPF filter should already have checked the length of the packet, but...
+ if (length < IPV6_HEADER_LEN) {
+ return;
+ }
+ Inet6Address destv6 = getIpv6DestinationAddress(recvbuf);
+ if (!destv6.isMulticastAddress()) {
+ return;
+ }
+ InetSocketAddress dest = new InetSocketAddress(destv6, 0);
+
+ FileDescriptor fd = null;
+ try {
+ fd = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_RAW);
+ SocketUtils.bindSocketToInterface(fd, mSendIfaceParams.name);
+
+ int ret = Os.sendto(fd, recvbuf, 0, length, 0, dest);
+ } catch (ErrnoException | SocketException e) {
+ Log.e(mTag, "handlePacket error: " + e);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+}
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
new file mode 100644
index 0000000..543a5c7
--- /dev/null
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -0,0 +1,659 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.net.util.TetheringUtils.getAllNodesForScopeId;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_RAW;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_NEIGHBOR;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.MacAddress;
+import android.net.TrafficStats;
+import android.net.util.InterfaceParams;
+import android.net.util.SocketUtils;
+import android.net.util.TetheringUtils;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.MtuOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.RdnssOption;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * Basic IPv6 Router Advertisement Daemon.
+ *
+ * TODO:
+ *
+ * - Rewrite using Handler (and friends) so that AlarmManager can deliver
+ * "kick" messages when it's time to send a multicast RA.
+ *
+ * @hide
+ */
+public class RouterAdvertisementDaemon {
+ private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName();
+
+ // Summary of various timers and lifetimes.
+ private static final int MIN_RTR_ADV_INTERVAL_SEC = 300;
+ private static final int MAX_RTR_ADV_INTERVAL_SEC = 600;
+ // In general, router, prefix, and DNS lifetimes are all advised to be
+ // greater than or equal to 3 * MAX_RTR_ADV_INTERVAL. Here, we double
+ // that to allow for multicast packet loss.
+ //
+ // This MAX_RTR_ADV_INTERVAL_SEC and DEFAULT_LIFETIME are also consistent
+ // with the https://tools.ietf.org/html/rfc7772#section-4 discussion of
+ // "approximately 7 RAs per hour".
+ private static final int DEFAULT_LIFETIME = 6 * MAX_RTR_ADV_INTERVAL_SEC;
+ // From https://tools.ietf.org/html/rfc4861#section-10 .
+ private static final int MIN_DELAY_BETWEEN_RAS_SEC = 3;
+ // Both initial and final RAs, but also for changes in RA contents.
+ // From https://tools.ietf.org/html/rfc4861#section-10 .
+ private static final int MAX_URGENT_RTR_ADVERTISEMENTS = 5;
+
+ private static final int DAY_IN_SECONDS = 86_400;
+
+ private final InterfaceParams mInterface;
+ private final InetSocketAddress mAllNodes;
+
+ // This lock is to protect the RA from being updated while being
+ // transmitted on another thread (multicast or unicast).
+ //
+ // TODO: This should be handled with a more RCU-like approach.
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final byte[] mRA = new byte[IPV6_MIN_MTU];
+ @GuardedBy("mLock")
+ private int mRaLength;
+ @GuardedBy("mLock")
+ private final DeprecatedInfoTracker mDeprecatedInfoTracker;
+ @GuardedBy("mLock")
+ private RaParams mRaParams;
+
+ private volatile FileDescriptor mSocket;
+ private volatile MulticastTransmitter mMulticastTransmitter;
+ private volatile UnicastResponder mUnicastResponder;
+
+ /** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
+ public static class RaParams {
+ // Tethered traffic will have the hop limit properly decremented.
+ // Consequently, set the hoplimit greater by one than the upstream
+ // unicast hop limit.
+ //
+ // TODO: Dynamically pass down the IPV6_UNICAST_HOPS value from the
+ // upstream interface for more correct behaviour.
+ static final byte DEFAULT_HOPLIMIT = 65;
+
+ public boolean hasDefaultRoute;
+ public byte hopLimit;
+ public int mtu;
+ public HashSet<IpPrefix> prefixes;
+ public HashSet<Inet6Address> dnses;
+
+ public RaParams() {
+ hasDefaultRoute = false;
+ hopLimit = DEFAULT_HOPLIMIT;
+ mtu = IPV6_MIN_MTU;
+ prefixes = new HashSet<IpPrefix>();
+ dnses = new HashSet<Inet6Address>();
+ }
+
+ public RaParams(RaParams other) {
+ hasDefaultRoute = other.hasDefaultRoute;
+ hopLimit = other.hopLimit;
+ mtu = other.mtu;
+ prefixes = (HashSet) other.prefixes.clone();
+ dnses = (HashSet) other.dnses.clone();
+ }
+
+ /**
+ * Returns the subset of RA parameters that become deprecated when
+ * moving from announcing oldRa to announcing newRa.
+ *
+ * Currently only tracks differences in |prefixes| and |dnses|.
+ */
+ public static RaParams getDeprecatedRaParams(RaParams oldRa, RaParams newRa) {
+ RaParams newlyDeprecated = new RaParams();
+
+ if (oldRa != null) {
+ for (IpPrefix ipp : oldRa.prefixes) {
+ if (newRa == null || !newRa.prefixes.contains(ipp)) {
+ newlyDeprecated.prefixes.add(ipp);
+ }
+ }
+
+ for (Inet6Address dns : oldRa.dnses) {
+ if (newRa == null || !newRa.dnses.contains(dns)) {
+ newlyDeprecated.dnses.add(dns);
+ }
+ }
+ }
+
+ return newlyDeprecated;
+ }
+ }
+
+ private static class DeprecatedInfoTracker {
+ private final HashMap<IpPrefix, Integer> mPrefixes = new HashMap<>();
+ private final HashMap<Inet6Address, Integer> mDnses = new HashMap<>();
+
+ Set<IpPrefix> getPrefixes() {
+ return mPrefixes.keySet();
+ }
+
+ void putPrefixes(Set<IpPrefix> prefixes) {
+ for (IpPrefix ipp : prefixes) {
+ mPrefixes.put(ipp, MAX_URGENT_RTR_ADVERTISEMENTS);
+ }
+ }
+
+ void removePrefixes(Set<IpPrefix> prefixes) {
+ for (IpPrefix ipp : prefixes) {
+ mPrefixes.remove(ipp);
+ }
+ }
+
+ Set<Inet6Address> getDnses() {
+ return mDnses.keySet();
+ }
+
+ void putDnses(Set<Inet6Address> dnses) {
+ for (Inet6Address dns : dnses) {
+ mDnses.put(dns, MAX_URGENT_RTR_ADVERTISEMENTS);
+ }
+ }
+
+ void removeDnses(Set<Inet6Address> dnses) {
+ for (Inet6Address dns : dnses) {
+ mDnses.remove(dns);
+ }
+ }
+
+ boolean isEmpty() {
+ return mPrefixes.isEmpty() && mDnses.isEmpty();
+ }
+
+ private boolean decrementCounters() {
+ boolean removed = decrementCounter(mPrefixes);
+ removed |= decrementCounter(mDnses);
+ return removed;
+ }
+
+ private <T> boolean decrementCounter(HashMap<T, Integer> map) {
+ boolean removed = false;
+
+ for (Iterator<Map.Entry<T, Integer>> it = map.entrySet().iterator();
+ it.hasNext();) {
+ Map.Entry<T, Integer> kv = it.next();
+ if (kv.getValue() == 0) {
+ it.remove();
+ removed = true;
+ } else {
+ kv.setValue(kv.getValue() - 1);
+ }
+ }
+
+ return removed;
+ }
+ }
+
+ public RouterAdvertisementDaemon(InterfaceParams ifParams) {
+ mInterface = ifParams;
+ mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
+ mDeprecatedInfoTracker = new DeprecatedInfoTracker();
+ }
+
+ /** Build new RA.*/
+ public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
+ synchronized (mLock) {
+ if (deprecatedParams != null) {
+ mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+ mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+ }
+
+ if (newParams != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+ mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+ }
+
+ mRaParams = newParams;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
+ }
+
+ /** Start router advertisement daemon. */
+ public boolean start() {
+ if (!createSocket()) {
+ return false;
+ }
+
+ mMulticastTransmitter = new MulticastTransmitter();
+ mMulticastTransmitter.start();
+
+ mUnicastResponder = new UnicastResponder();
+ mUnicastResponder.start();
+
+ return true;
+ }
+
+ /** Stop router advertisement daemon. */
+ public void stop() {
+ closeSocket();
+ // Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
+ // the thread's termination.
+ maybeNotifyMulticastTransmitter();
+ mMulticastTransmitter = null;
+ mUnicastResponder = null;
+ }
+
+ @GuardedBy("mLock")
+ private void assembleRaLocked() {
+ final ByteBuffer ra = ByteBuffer.wrap(mRA);
+ ra.order(ByteOrder.BIG_ENDIAN);
+
+ final boolean haveRaParams = (mRaParams != null);
+ boolean shouldSendRA = false;
+
+ try {
+ putHeader(ra, haveRaParams && mRaParams.hasDefaultRoute,
+ haveRaParams ? mRaParams.hopLimit : RaParams.DEFAULT_HOPLIMIT);
+ putSlla(ra, mInterface.macAddr.toByteArray());
+ mRaLength = ra.position();
+
+ // https://tools.ietf.org/html/rfc5175#section-4 says:
+ //
+ // "MUST NOT be added to a Router Advertisement message
+ // if no flags in the option are set."
+ //
+ // putExpandedFlagsOption(ra);
+
+ if (haveRaParams) {
+ putMtu(ra, mRaParams.mtu);
+ mRaLength = ra.position();
+
+ for (IpPrefix ipp : mRaParams.prefixes) {
+ putPio(ra, ipp, DEFAULT_LIFETIME, DEFAULT_LIFETIME);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+
+ if (mRaParams.dnses.size() > 0) {
+ putRdnss(ra, mRaParams.dnses, DEFAULT_LIFETIME);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+ }
+
+ for (IpPrefix ipp : mDeprecatedInfoTracker.getPrefixes()) {
+ putPio(ra, ipp, 0, 0);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+
+ final Set<Inet6Address> deprecatedDnses = mDeprecatedInfoTracker.getDnses();
+ if (!deprecatedDnses.isEmpty()) {
+ putRdnss(ra, deprecatedDnses, 0);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+ } catch (BufferOverflowException e) {
+ // The packet up to mRaLength is valid, since it has been updated
+ // progressively as the RA was built. Log an error, and continue
+ // on as best as possible.
+ Log.e(TAG, "Could not construct new RA: " + e);
+ }
+
+ // We have nothing worth announcing; indicate as much to maybeSendRA().
+ if (!shouldSendRA) {
+ mRaLength = 0;
+ }
+ }
+
+ private void maybeNotifyMulticastTransmitter() {
+ final MulticastTransmitter m = mMulticastTransmitter;
+ if (m != null) {
+ m.hup();
+ }
+ }
+
+ private static byte asByte(int value) {
+ return (byte) value;
+ }
+ private static short asShort(int value) {
+ return (short) value;
+ }
+
+ private static void putHeader(ByteBuffer ra, boolean hasDefaultRoute, byte hopLimit) {
+ // RFC 4191 "high" preference, iff. advertising a default route.
+ final byte flags = hasDefaultRoute ? asByte(0x08) : asByte(0);
+ final short lifetime = hasDefaultRoute ? asShort(DEFAULT_LIFETIME) : asShort(0);
+ final Icmpv6Header icmpv6Header =
+ new Icmpv6Header(asByte(ICMPV6_ROUTER_ADVERTISEMENT) /* type */,
+ asByte(0) /* code */, asShort(0) /* checksum */);
+ final RaHeader raHeader = new RaHeader(hopLimit, flags, lifetime, 0 /* reachableTime */,
+ 0 /* retransTimer */);
+ icmpv6Header.writeToByteBuffer(ra);
+ raHeader.writeToByteBuffer(ra);
+ }
+
+ private static void putSlla(ByteBuffer ra, byte[] slla) {
+ if (slla == null || slla.length != 6) {
+ // Only IEEE 802.3 6-byte addresses are supported.
+ return;
+ }
+
+ final ByteBuffer sllaOption = LlaOption.build(asByte(ICMPV6_ND_OPTION_SLLA),
+ MacAddress.fromBytes(slla));
+ ra.put(sllaOption);
+ }
+
+ private static void putExpandedFlagsOption(ByteBuffer ra) {
+ /**
+ Router Advertisement Expanded Flags Option
+
+ 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
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Bit fields available ..
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ ... for assignment |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+
+ final byte nd_option__efo = 26;
+ final byte efo_num_8octets = 1;
+
+ ra.put(nd_option__efo)
+ .put(efo_num_8octets)
+ .putShort(asShort(0))
+ .putInt(0);
+ }
+
+ private static void putMtu(ByteBuffer ra, int mtu) {
+ final ByteBuffer mtuOption = MtuOption.build((mtu < IPV6_MIN_MTU) ? IPV6_MIN_MTU : mtu);
+ ra.put(mtuOption);
+ }
+
+ private static void putPio(ByteBuffer ra, IpPrefix ipp,
+ int validTime, int preferredTime) {
+ final int prefixLength = ipp.getPrefixLength();
+ if (prefixLength != 64) {
+ return;
+ }
+
+ if (validTime < 0) validTime = 0;
+ if (preferredTime < 0) preferredTime = 0;
+ if (preferredTime > validTime) preferredTime = validTime;
+
+ final ByteBuffer pioOption = PrefixInformationOption.build(ipp,
+ asByte(PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), validTime, preferredTime);
+ ra.put(pioOption);
+ }
+
+ private static void putRio(ByteBuffer ra, IpPrefix ipp) {
+ /**
+ Route Information Option
+
+ 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
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Prefix Length |Resvd|Prf|Resvd|
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Route Lifetime |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Prefix (Variable Length) |
+ . .
+ . .
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ final int prefixLength = ipp.getPrefixLength();
+ if (prefixLength > 64) {
+ return;
+ }
+ final byte nd_option_rio = 24;
+ final byte rio_num_8octets = asByte(
+ (prefixLength == 0) ? 1 : (prefixLength <= 8) ? 2 : 3);
+
+ final byte[] addr = ipp.getAddress().getAddress();
+ ra.put(nd_option_rio)
+ .put(rio_num_8octets)
+ .put(asByte(prefixLength))
+ .put(asByte(0x18))
+ .putInt(DEFAULT_LIFETIME);
+
+ // Rely upon an IpPrefix's address being properly zeroed.
+ if (prefixLength > 0) {
+ ra.put(addr, 0, (prefixLength <= 64) ? 8 : 16);
+ }
+ }
+
+ private static void putRdnss(ByteBuffer ra, Set<Inet6Address> dnses, int lifetime) {
+ final HashSet<Inet6Address> filteredDnses = new HashSet<>();
+ for (Inet6Address dns : dnses) {
+ if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) {
+ filteredDnses.add(dns);
+ }
+ }
+ if (filteredDnses.isEmpty()) return;
+
+ final Inet6Address[] dnsesArray =
+ filteredDnses.toArray(new Inet6Address[filteredDnses.size()]);
+ final ByteBuffer rdnssOption = RdnssOption.build(lifetime, dnsesArray);
+ // NOTE: If the full of list DNS servers doesn't fit in the packet,
+ // this code will cause a buffer overflow and the RA won't include
+ // this instance of the option at all.
+ //
+ // TODO: Consider looking at ra.remaining() to determine how many
+ // DNS servers will fit, and adding only those.
+ ra.put(rdnssOption);
+ }
+
+ private boolean createSocket() {
+ final int send_timout_ms = 300;
+
+ final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
+ try {
+ mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ // Setting SNDTIMEO is purely for defensive purposes.
+ Os.setsockoptTimeval(
+ mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
+ SocketUtils.bindSocketToInterface(mSocket, mInterface.name);
+ TetheringUtils.setupRaSocket(mSocket, mInterface.index);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Failed to create RA daemon socket: " + e);
+ return false;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
+ }
+
+ return true;
+ }
+
+ private void closeSocket() {
+ if (mSocket != null) {
+ try {
+ SocketUtils.closeSocket(mSocket);
+ } catch (IOException ignored) { }
+ }
+ mSocket = null;
+ }
+
+ private boolean isSocketValid() {
+ final FileDescriptor s = mSocket;
+ return (s != null) && s.valid();
+ }
+
+ private boolean isSuitableDestination(InetSocketAddress dest) {
+ if (mAllNodes.equals(dest)) {
+ return true;
+ }
+
+ final InetAddress destip = dest.getAddress();
+ return (destip instanceof Inet6Address)
+ && destip.isLinkLocalAddress()
+ && (((Inet6Address) destip).getScopeId() == mInterface.index);
+ }
+
+ private void maybeSendRA(InetSocketAddress dest) {
+ if (dest == null || !isSuitableDestination(dest)) {
+ dest = mAllNodes;
+ }
+
+ try {
+ synchronized (mLock) {
+ if (mRaLength < ICMPV6_RA_HEADER_LEN) {
+ // No actual RA to send.
+ return;
+ }
+ Os.sendto(mSocket, mRA, 0, mRaLength, 0, dest);
+ }
+ Log.d(TAG, "RA sendto " + dest.getAddress().getHostAddress());
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "sendto error: " + e);
+ }
+ }
+ }
+
+ private final class UnicastResponder extends Thread {
+ private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
+
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ // Blocking receive.
+ final int rval = Os.recvfrom(
+ mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
+ // Do the least possible amount of validation.
+ if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+ continue;
+ }
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "recvfrom error: " + e);
+ }
+ continue;
+ }
+
+ maybeSendRA(mSolicitor);
+ }
+ }
+ }
+
+ // TODO: Consider moving this to run on a provided Looper as a Handler,
+ // with WakeupMessage-style messages providing the timer driven input.
+ private final class MulticastTransmitter extends Thread {
+ private final Random mRandom = new Random();
+ private final AtomicInteger mUrgentAnnouncements = new AtomicInteger(0);
+
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ Thread.sleep(getNextMulticastTransmitDelayMs());
+ } catch (InterruptedException ignored) {
+ // Stop sleeping, immediately send an RA, and continue.
+ }
+
+ maybeSendRA(mAllNodes);
+ synchronized (mLock) {
+ if (mDeprecatedInfoTracker.decrementCounters()) {
+ // At least one deprecated PIO has been removed;
+ // reassemble the RA.
+ assembleRaLocked();
+ }
+ }
+ }
+ }
+
+ public void hup() {
+ // Set to one fewer that the desired number, because as soon as
+ // the thread interrupt is processed we immediately send an RA
+ // and mUrgentAnnouncements is not examined until the subsequent
+ // sleep interval computation (i.e. this way we send 3 and not 4).
+ mUrgentAnnouncements.set(MAX_URGENT_RTR_ADVERTISEMENTS - 1);
+ interrupt();
+ }
+
+ private int getNextMulticastTransmitDelaySec() {
+ boolean deprecationInProgress = false;
+ synchronized (mLock) {
+ if (mRaLength < ICMPV6_RA_HEADER_LEN) {
+ // No actual RA to send; just sleep for 1 day.
+ return DAY_IN_SECONDS;
+ }
+ deprecationInProgress = !mDeprecatedInfoTracker.isEmpty();
+ }
+
+ final int urgentPending = mUrgentAnnouncements.getAndDecrement();
+ if ((urgentPending > 0) || deprecationInProgress) {
+ return MIN_DELAY_BETWEEN_RAS_SEC;
+ }
+
+ return MIN_RTR_ADV_INTERVAL_SEC + mRandom.nextInt(
+ MAX_RTR_ADV_INTERVAL_SEC - MIN_RTR_ADV_INTERVAL_SEC);
+ }
+
+ private long getNextMulticastTransmitDelayMs() {
+ return 1000 * (long) getNextMulticastTransmitDelaySec();
+ }
+ }
+}
diff --git a/Tethering/src/android/net/util/InterfaceSet.java b/Tethering/src/android/net/util/InterfaceSet.java
new file mode 100644
index 0000000..7589787
--- /dev/null
+++ b/Tethering/src/android/net/util/InterfaceSet.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.StringJoiner;
+
+
+/**
+ * @hide
+ */
+public class InterfaceSet {
+ public final Set<String> ifnames;
+
+ public InterfaceSet(String... names) {
+ final Set<String> nameSet = new HashSet<>();
+ for (String name : names) {
+ if (name != null) nameSet.add(name);
+ }
+ ifnames = Collections.unmodifiableSet(nameSet);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner sj = new StringJoiner(",", "[", "]");
+ for (String ifname : ifnames) sj.add(ifname);
+ return sj.toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj != null
+ && obj instanceof InterfaceSet
+ && ifnames.equals(((InterfaceSet) obj).ifnames);
+ }
+}
diff --git a/Tethering/src/android/net/util/PrefixUtils.java b/Tethering/src/android/net/util/PrefixUtils.java
new file mode 100644
index 0000000..f203e99
--- /dev/null
+++ b/Tethering/src/android/net/util/PrefixUtils.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * @hide
+ */
+public class PrefixUtils {
+ private static final IpPrefix[] MIN_NON_FORWARDABLE_PREFIXES = {
+ pfx("127.0.0.0/8"), // IPv4 loopback
+ pfx("169.254.0.0/16"), // IPv4 link-local, RFC3927#section-8
+ pfx("::/3"),
+ pfx("fe80::/64"), // IPv6 link-local
+ pfx("fc00::/7"), // IPv6 ULA
+ pfx("ff02::/8"), // IPv6 link-local multicast
+ };
+
+ public static final IpPrefix DEFAULT_WIFI_P2P_PREFIX = pfx("192.168.49.0/24");
+
+ /** Get non forwardable prefixes. */
+ public static Set<IpPrefix> getNonForwardablePrefixes() {
+ final HashSet<IpPrefix> prefixes = new HashSet<>();
+ addNonForwardablePrefixes(prefixes);
+ return prefixes;
+ }
+
+ /** Add non forwardable prefixes. */
+ public static void addNonForwardablePrefixes(Set<IpPrefix> prefixes) {
+ Collections.addAll(prefixes, MIN_NON_FORWARDABLE_PREFIXES);
+ }
+
+ /** Get local prefixes from |lp|. */
+ public static Set<IpPrefix> localPrefixesFrom(LinkProperties lp) {
+ final HashSet<IpPrefix> localPrefixes = new HashSet<>();
+ if (lp == null) return localPrefixes;
+
+ for (LinkAddress addr : lp.getAllLinkAddresses()) {
+ if (addr.getAddress().isLinkLocalAddress()) continue;
+ localPrefixes.add(asIpPrefix(addr));
+ }
+ // TODO: Add directly-connected routes as well (ones from which we did
+ // not also form a LinkAddress)?
+
+ return localPrefixes;
+ }
+
+ /** Convert LinkAddress |addr| to IpPrefix. */
+ public static IpPrefix asIpPrefix(LinkAddress addr) {
+ return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
+ }
+
+ /** Convert InetAddress |ip| to IpPrefix. */
+ public static IpPrefix ipAddressAsPrefix(InetAddress ip) {
+ final int bitLength = (ip instanceof Inet4Address)
+ ? NetworkConstants.IPV4_ADDR_BITS
+ : NetworkConstants.IPV6_ADDR_BITS;
+ return new IpPrefix(ip, bitLength);
+ }
+
+ private static IpPrefix pfx(String prefixStr) {
+ return new IpPrefix(prefixStr);
+ }
+}
diff --git a/Tethering/src/android/net/util/TetheringMessageBase.java b/Tethering/src/android/net/util/TetheringMessageBase.java
new file mode 100644
index 0000000..29c0a81
--- /dev/null
+++ b/Tethering/src/android/net/util/TetheringMessageBase.java
@@ -0,0 +1,25 @@
+/*
+ * 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 android.net.util;
+
+/**
+ * This class defines Message.what base addresses for various state machine.
+ */
+public class TetheringMessageBase {
+ public static final int BASE_MAIN_SM = 0;
+ public static final int BASE_IPSERVER = 100;
+
+}
diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java
new file mode 100644
index 0000000..29900d9
--- /dev/null
+++ b/Tethering/src/android/net/util/TetheringUtils.java
@@ -0,0 +1,179 @@
+/*
+ * 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 android.net.util;
+
+import android.net.TetherStatsParcel;
+import android.net.TetheringRequestParcel;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.networkstack.tethering.TetherStatsValue;
+
+import java.io.FileDescriptor;
+import java.net.Inet6Address;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * The classes and the methods for tethering utilization.
+ *
+ * {@hide}
+ */
+public class TetheringUtils {
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ public static final byte[] ALL_NODES = new byte[] {
+ (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
+ };
+
+ /**
+ * Configures a socket for receiving and sending ICMPv6 neighbor advertisments.
+ * @param fd the socket's {@link FileDescriptor}.
+ */
+ public static native void setupNaSocket(FileDescriptor fd)
+ throws SocketException;
+
+ /**
+ * Configures a socket for receiving and sending ICMPv6 neighbor solicitations.
+ * @param fd the socket's {@link FileDescriptor}.
+ */
+ public static native void setupNsSocket(FileDescriptor fd)
+ throws SocketException;
+
+ /**
+ * The object which records offload Tx/Rx forwarded bytes/packets.
+ * TODO: Replace the inner class ForwardedStats of class OffloadHardwareInterface with
+ * this class as well.
+ */
+ public static class ForwardedStats {
+ public final long rxBytes;
+ public final long rxPackets;
+ public final long txBytes;
+ public final long txPackets;
+
+ public ForwardedStats() {
+ rxBytes = 0;
+ rxPackets = 0;
+ txBytes = 0;
+ txPackets = 0;
+ }
+
+ public ForwardedStats(long rxBytes, long txBytes) {
+ this.rxBytes = rxBytes;
+ this.rxPackets = 0;
+ this.txBytes = txBytes;
+ this.txPackets = 0;
+ }
+
+ public ForwardedStats(long rxBytes, long rxPackets, long txBytes, long txPackets) {
+ this.rxBytes = rxBytes;
+ this.rxPackets = rxPackets;
+ this.txBytes = txBytes;
+ this.txPackets = txPackets;
+ }
+
+ public ForwardedStats(@NonNull TetherStatsParcel tetherStats) {
+ rxBytes = tetherStats.rxBytes;
+ rxPackets = tetherStats.rxPackets;
+ txBytes = tetherStats.txBytes;
+ txPackets = tetherStats.txPackets;
+ }
+
+ public ForwardedStats(@NonNull TetherStatsValue tetherStats) {
+ rxBytes = tetherStats.rxBytes;
+ rxPackets = tetherStats.rxPackets;
+ txBytes = tetherStats.txBytes;
+ txPackets = tetherStats.txPackets;
+ }
+
+ public ForwardedStats(@NonNull ForwardedStats other) {
+ rxBytes = other.rxBytes;
+ rxPackets = other.rxPackets;
+ txBytes = other.txBytes;
+ txPackets = other.txPackets;
+ }
+
+ /** Add Tx/Rx bytes/packets and return the result as a new object. */
+ @NonNull
+ public ForwardedStats add(@NonNull ForwardedStats other) {
+ return new ForwardedStats(rxBytes + other.rxBytes, rxPackets + other.rxPackets,
+ txBytes + other.txBytes, txPackets + other.txPackets);
+ }
+
+ /** Subtract Tx/Rx bytes/packets and return the result as a new object. */
+ @NonNull
+ public ForwardedStats subtract(@NonNull ForwardedStats other) {
+ // TODO: Perhaps throw an exception if any negative difference value just in case.
+ final long rxBytesDiff = Math.max(rxBytes - other.rxBytes, 0);
+ final long rxPacketsDiff = Math.max(rxPackets - other.rxPackets, 0);
+ final long txBytesDiff = Math.max(txBytes - other.txBytes, 0);
+ final long txPacketsDiff = Math.max(txPackets - other.txPackets, 0);
+ return new ForwardedStats(rxBytesDiff, rxPacketsDiff, txBytesDiff, txPacketsDiff);
+ }
+
+ /** Returns the string representation of this object. */
+ @NonNull
+ public String toString() {
+ return String.format("ForwardedStats(rxb: %d, rxp: %d, txb: %d, txp: %d)", rxBytes,
+ rxPackets, txBytes, txPackets);
+ }
+ }
+
+ /**
+ * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
+ * @param fd the socket's {@link FileDescriptor}.
+ * @param ifIndex the interface index.
+ */
+ public static native void setupRaSocket(FileDescriptor fd, int ifIndex)
+ throws SocketException;
+
+ /**
+ * Read s as an unsigned 16-bit integer.
+ */
+ public static int uint16(short s) {
+ 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 {
+ return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
+ } catch (UnknownHostException uhe) {
+ Log.wtf("TetheringUtils", "Failed to construct Inet6Address from "
+ + Arrays.toString(ALL_NODES) + " and scopedId " + scopeId);
+ return null;
+ }
+ }
+}
diff --git a/Tethering/src/android/net/util/VersionedBroadcastListener.java b/Tethering/src/android/net/util/VersionedBroadcastListener.java
new file mode 100644
index 0000000..e2804ab
--- /dev/null
+++ b/Tethering/src/android/net/util/VersionedBroadcastListener.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+
+/**
+ * A utility class that runs the provided callback on the provided handler when
+ * intents matching the provided filter arrive. Intents received by a stale
+ * receiver are safely ignored.
+ *
+ * Calls to startListening() and stopListening() must happen on the same thread.
+ *
+ * @hide
+ */
+public class VersionedBroadcastListener {
+ private static final boolean DBG = false;
+
+ private final String mTag;
+ private final Context mContext;
+ private final Handler mHandler;
+ private final IntentFilter mFilter;
+ private final Consumer<Intent> mCallback;
+ private final AtomicInteger mGenerationNumber;
+ private BroadcastReceiver mReceiver;
+
+ public VersionedBroadcastListener(String tag, Context ctx, Handler handler,
+ IntentFilter filter, Consumer<Intent> callback) {
+ mTag = tag;
+ mContext = ctx;
+ mHandler = handler;
+ mFilter = filter;
+ mCallback = callback;
+ mGenerationNumber = new AtomicInteger(0);
+ }
+
+ /** Start listening to intent broadcast. */
+ public void startListening() {
+ if (DBG) Log.d(mTag, "startListening");
+ if (mReceiver != null) return;
+
+ mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
+ mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+ }
+
+ /** Stop listening to intent broadcast. */
+ public void stopListening() {
+ if (DBG) Log.d(mTag, "stopListening");
+ if (mReceiver == null) return;
+
+ mGenerationNumber.incrementAndGet();
+ mContext.unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+
+ private static class Receiver extends BroadcastReceiver {
+ public final String tag;
+ public final AtomicInteger atomicGenerationNumber;
+ public final Consumer<Intent> callback;
+ // Used to verify this receiver is still current.
+ public final int generationNumber;
+
+ Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
+ this.tag = tag;
+ this.atomicGenerationNumber = atomicGenerationNumber;
+ this.callback = callback;
+ generationNumber = atomicGenerationNumber.incrementAndGet();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final int currentGenerationNumber = atomicGenerationNumber.get();
+
+ if (DBG) {
+ Log.d(tag, "receiver generationNumber=" + generationNumber
+ + ", current generationNumber=" + currentGenerationNumber);
+ }
+ if (generationNumber != currentGenerationNumber) return;
+
+ callback.accept(intent);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
new file mode 100644
index 0000000..8adcbd9
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -0,0 +1,1577 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
+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;
+
+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 android.app.usage.NetworkStatsManager;
+import android.net.INetd;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.NetworkStats;
+import android.net.NetworkStats.Entry;
+import android.net.TetherOffloadRuleParcel;
+import android.net.ip.ConntrackMonitor;
+import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
+import android.net.ip.IpServer;
+import android.net.netlink.NetlinkConstants;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.net.util.TetheringUtils.ForwardedStats;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This coordinator is responsible for providing BPF offload relevant functionality.
+ * - Get tethering stats.
+ * - Set data limit.
+ * - Set global alert.
+ * - Add/remove forwarding rules.
+ *
+ * @hide
+ */
+public class BpfCoordinator {
+ // Ensure the JNI code is loaded. In production this will already have been loaded by
+ // TetherService, but for tests it needs to be either loaded here or loaded by every test.
+ // TODO: is there a better way?
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ private static final String TAG = BpfCoordinator.class.getSimpleName();
+ private static final int DUMP_TIMEOUT_MS = 10_000;
+ private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
+ "00:00:00:00:00:00");
+ private static final String TETHER_DOWNSTREAM4_MAP_PATH = makeMapPath(DOWNSTREAM, 4);
+ private static final String TETHER_UPSTREAM4_MAP_PATH = makeMapPath(UPSTREAM, 4);
+ private static final String TETHER_DOWNSTREAM6_FS_PATH = makeMapPath(DOWNSTREAM, 6);
+ private static final String TETHER_UPSTREAM6_FS_PATH = makeMapPath(UPSTREAM, 6);
+ private static final String TETHER_STATS_MAP_PATH = makeMapPath("stats");
+ private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
+ private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
+ private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev");
+
+ /** The names of all the BPF counters defined in bpf_tethering.h. */
+ public static final String[] sBpfCounterNames = getBpfCounterNames();
+
+ private static String makeMapPath(String which) {
+ return "/sys/fs/bpf/tethering/map_offload_tether_" + which + "_map";
+ }
+
+ private static String makeMapPath(boolean downstream, int ipVersion) {
+ return makeMapPath((downstream ? "downstream" : "upstream") + ipVersion);
+ }
+
+ @VisibleForTesting
+ enum StatsType {
+ STATS_PER_IFACE,
+ STATS_PER_UID,
+ }
+
+ @NonNull
+ private final Handler mHandler;
+ @NonNull
+ private final INetd mNetd;
+ @NonNull
+ private final SharedLog mLog;
+ @NonNull
+ private final Dependencies mDeps;
+ @NonNull
+ private final ConntrackMonitor mConntrackMonitor;
+ @Nullable
+ private final BpfTetherStatsProvider mStatsProvider;
+ @NonNull
+ private final BpfCoordinatorShim mBpfCoordinatorShim;
+ @NonNull
+ private final BpfConntrackEventConsumer mBpfConntrackEventConsumer;
+
+ // 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
+ // in the constructor because it is hard to unwind all existing change once device
+ // configuration is changed. Especially the forwarding rules. Keep the same setting
+ // 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;
+
+ // Maps upstream interface index to offloaded traffic statistics.
+ // Always contains the latest total bytes/packets, since each upstream was started, received
+ // from the BPF maps for each interface.
+ private final SparseArray<ForwardedStats> mStats = new SparseArray<>();
+
+ // Maps upstream interface names to interface quotas.
+ // Always contains the latest value received from the framework for each interface, regardless
+ // of whether offload is currently running (or is even supported) on that interface. Only
+ // includes interfaces that have a quota set. Note that this map is used for storing the quota
+ // which is set from the service. Because the service uses the interface name to present the
+ // interface, this map uses the interface name to be the mapping index.
+ private final HashMap<String, Long> mInterfaceQuotas = new HashMap<>();
+
+ // Maps upstream interface index to interface names.
+ // Store all interface name since boot. Used for lookup what interface name it is from the
+ // tether stats got from netd because netd reports interface index to present an interface.
+ // TODO: Remove the unused interface name.
+ private final SparseArray<String> mInterfaceNames = new SparseArray<>();
+
+ // Map of downstream rule maps. Each of these maps represents the IPv6 forwarding rules for a
+ // given downstream. Each map:
+ // - Is owned by the IpServer that is responsible for that downstream.
+ // - Must only be modified by that IpServer.
+ // - Is created when the IpServer adds its first rule, and deleted when the IpServer deletes
+ // its last rule (or clears its rules).
+ // TODO: Perhaps seal the map and rule operations which communicates with netd into a class.
+ // TODO: Does this need to be a LinkedHashMap or can it just be a HashMap? Also, could it be
+ // a ConcurrentHashMap, in order to avoid the copies in tetherOffloadRuleClear
+ // and tetherOffloadRuleUpdate?
+ // TODO: Perhaps use one-dimensional map and access specific downstream rules via downstream
+ // index. For doing that, IpServer must guarantee that it always has a valid IPv6 downstream
+ // interface index while calling function to clear all rules. IpServer may be calling clear
+ // rules function without a valid IPv6 downstream interface index even if it may have one
+ // before. IpServer would need to call getInterfaceParams() in the constructor instead of when
+ // startIpv6() is called, and make mInterfaceParams final.
+ private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
+ mIpv6ForwardingRules = new LinkedHashMap<>();
+
+ // Map of downstream client maps. Each of these maps represents the IPv4 clients for a given
+ // downstream. Needed to build IPv4 forwarding rules when conntrack events are received.
+ // Each map:
+ // - Is owned by the IpServer that is responsible for that downstream.
+ // - Must only be modified by that IpServer.
+ // - Is created when the IpServer adds its first client, and deleted when the IpServer deletes
+ // its last client.
+ // Note that relying on the client address for finding downstream is okay for now because the
+ // client address is unique. See PrivateAddressCoordinator#requestDownstreamAddress.
+ // TODO: Refactor if any possible that the client address is not unique.
+ 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.
+ private final HashMap<Inet4Address, Integer> mIpv4UpstreamIndices = new HashMap<>();
+
+ // Map for upstream and downstream pair.
+ private final HashMap<String, HashSet<String>> mForwardingPairs = new HashMap<>();
+
+ // Set for upstream and downstream device map. Used for caching BPF dev map status and
+ // reduce duplicate adding or removing map operations. Use LinkedHashSet because the test
+ // BpfCoordinatorTest needs predictable iteration order.
+ private final Set<Integer> mDeviceMapSet = new LinkedHashSet<>();
+
+ // Runnable that used by scheduling next polling of stats.
+ private final Runnable mScheduledPollingTask = () -> {
+ updateForwardedStats();
+ maybeSchedulePollingStats();
+ };
+
+ // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
+ @VisibleForTesting
+ public abstract static class Dependencies {
+ /** Get handler. */
+ @NonNull public abstract Handler getHandler();
+
+ /** Get netd. */
+ @NonNull public abstract INetd getNetd();
+
+ /** Get network stats manager. */
+ @NonNull public abstract NetworkStatsManager getNetworkStatsManager();
+
+ /** Get shared log. */
+ @NonNull public abstract SharedLog getSharedLog();
+
+ /** Get tethering configuration. */
+ @Nullable public abstract TetheringConfiguration getTetherConfig();
+
+ /** Get conntrack monitor. */
+ @NonNull public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) {
+ return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
+ }
+
+ /** Get interface information for a given interface. */
+ @NonNull public InterfaceParams getInterfaceParams(String ifName) {
+ return InterfaceParams.getByName(ifName);
+ }
+
+ /**
+ * Check OS Build at least S.
+ *
+ * TODO: move to BpfCoordinatorShim once the test doesn't need the mocked OS build for
+ * testing different code flows concurrently.
+ */
+ public boolean isAtLeastS() {
+ // TODO: consider using ShimUtils.isAtLeastS.
+ return SdkLevel.isAtLeastS();
+ }
+
+ /** Get downstream4 BPF map. */
+ @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
+ BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create downstream4 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get upstream4 BPF map. */
+ @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
+ BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create upstream4 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get downstream6 BPF map. */
+ @Nullable public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
+ BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create downstream6 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get upstream6 BPF map. */
+ @Nullable public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TetherUpstream6Key.class, Tether6Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create upstream6 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get stats BPF map. */
+ @Nullable public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_STATS_MAP_PATH,
+ BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create stats map: " + e);
+ return null;
+ }
+ }
+
+ /** Get limit BPF map. */
+ @Nullable public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
+ BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create limit map: " + e);
+ return null;
+ }
+ }
+
+ /** Get dev BPF map. */
+ @Nullable public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_DEV_MAP_PATH,
+ BpfMap.BPF_F_RDWR, TetherDevKey.class, TetherDevValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create dev map: " + e);
+ return null;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public BpfCoordinator(@NonNull Dependencies deps) {
+ mDeps = deps;
+ mHandler = mDeps.getHandler();
+ mNetd = mDeps.getNetd();
+ mLog = mDeps.getSharedLog().forSubComponent(TAG);
+ mIsBpfEnabled = isBpfEnabled();
+
+ // The conntrack consummer needs to be initialized in BpfCoordinator constructor because it
+ // have to access the data members of BpfCoordinator which is not a static class. The
+ // consumer object is also needed for initializing the conntrack monitor which may be
+ // mocked for testing.
+ mBpfConntrackEventConsumer = new BpfConntrackEventConsumer();
+ mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer);
+
+ BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
+ try {
+ mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
+ getClass().getSimpleName(), provider);
+ } catch (RuntimeException e) {
+ // TODO: Perhaps not allow to use BPF offload because the reregistration failure
+ // implied that no data limit could be applies on a metered upstream if any.
+ Log.wtf(TAG, "Cannot register offload stats provider: " + e);
+ provider = null;
+ }
+ mStatsProvider = provider;
+
+ mBpfCoordinatorShim = BpfCoordinatorShim.getBpfCoordinatorShim(deps);
+ if (!mBpfCoordinatorShim.isInitialized()) {
+ mLog.e("Bpf shim not initialized");
+ }
+ }
+
+ /**
+ * Start BPF tethering offload stats polling when the first upstream is started.
+ * 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;
+ }
+
+ mPollingStarted = true;
+ maybeSchedulePollingStats();
+
+ mLog.i("Polling started");
+ }
+
+ /**
+ * Stop BPF tethering offload stats polling.
+ * The data limit cleanup and the tether stats maps cleanup are not implemented here.
+ * These cleanups rely on all IpServers calling #tetherOffloadRuleRemove. After the
+ * last rule is removed from the upstream, #tetherOffloadRuleRemove does the cleanup
+ * functionality.
+ * Note that this can be only called on handler thread.
+ */
+ public void stopPolling() {
+ if (!mPollingStarted) return;
+
+ // Stop scheduled polling tasks and poll the latest stats from BPF maps.
+ if (mHandler.hasCallbacks(mScheduledPollingTask)) {
+ mHandler.removeCallbacks(mScheduledPollingTask);
+ }
+ updateForwardedStats();
+ mPollingStarted = false;
+
+ mLog.i("Polling stopped");
+ }
+
+ private boolean isUsingBpf() {
+ return mIsBpfEnabled && mBpfCoordinatorShim.isInitialized();
+ }
+
+ /**
+ * 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.
+ * +---------------------------------------------------------------------------+
+ * | ERROR unparsable netlink msg: 1400000001010103000000000000000002000000 |
+ * +------------------+--------------------------------------------------------+
+ * | | struct nlmsghdr |
+ * | 14000000 | length = 20 |
+ * | 0101 | type = NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_GET |
+ * | 0103 | flags |
+ * | 00000000 | seqno = 0 |
+ * | 00000000 | pid = 0 |
+ * | | struct nfgenmsg |
+ * | 02 | nfgen_family = AF_INET |
+ * | 00 | version = NFNETLINK_V0 |
+ * | 0000 | res_id |
+ * +------------------+--------------------------------------------------------+
+ * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage.
+ */
+ public void startMonitoring(@NonNull final IpServer ipServer) {
+ // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim.
+ if (!isUsingBpf() || !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);
+ }
+
+ /**
+ * Stop conntrack event monitoring.
+ * Note that this can be only called on handler thread.
+ */
+ public void stopMonitoring(@NonNull final IpServer ipServer) {
+ // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim.
+ if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+
+ mMonitoringIpServers.remove(ipServer);
+
+ if (!mMonitoringIpServers.isEmpty()) return;
+
+ mConntrackMonitor.stop();
+ mLog.i("Monitoring stopped");
+ }
+
+ /**
+ * Add forwarding rule. After adding the first rule on a given upstream, must add the data
+ * limit on the given upstream.
+ * Note that this can be only called on handler thread.
+ */
+ public void tetherOffloadRuleAdd(
+ @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
+ if (!isUsingBpf()) return;
+
+ // TODO: Perhaps avoid to add a duplicate rule.
+ if (!mBpfCoordinatorShim.tetherOffloadRuleAdd(rule)) return;
+
+ if (!mIpv6ForwardingRules.containsKey(ipServer)) {
+ mIpv6ForwardingRules.put(ipServer, new LinkedHashMap<Inet6Address,
+ Ipv6ForwardingRule>());
+ }
+ LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
+
+ // Add upstream and downstream interface index to dev map.
+ maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex);
+
+ // When the first rule is added to an upstream, setup upstream forwarding and data limit.
+ maybeSetLimit(rule.upstreamIfindex);
+
+ if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
+ final int downstream = rule.downstreamIfindex;
+ final int upstream = rule.upstreamIfindex;
+ // TODO: support upstream forwarding on non-point-to-point interfaces.
+ // TODO: get the MTU from LinkProperties and update the rules when it changes.
+ if (!mBpfCoordinatorShim.startUpstreamIpv6Forwarding(downstream, upstream, rule.srcMac,
+ NULL_MAC_ADDRESS, NULL_MAC_ADDRESS, NetworkStackConstants.ETHER_MTU)) {
+ mLog.e("Failed to enable upstream IPv6 forwarding from "
+ + mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream));
+ }
+ }
+
+ // Must update the adding rule after calling #isAnyRuleOnUpstream because it needs to
+ // check if it is about adding a first rule for a given upstream.
+ rules.put(rule.address, rule);
+ }
+
+ /**
+ * Remove forwarding rule. After removing the last rule on a given upstream, must clear
+ * data limit, update the last tether stats and remove the tether stats in the BPF maps.
+ * Note that this can be only called on handler thread.
+ */
+ public void tetherOffloadRuleRemove(
+ @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
+ if (!isUsingBpf()) return;
+
+ if (!mBpfCoordinatorShim.tetherOffloadRuleRemove(rule)) return;
+
+ LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
+ if (rules == null) return;
+
+ // Must remove rules before calling #isAnyRuleOnUpstream because it needs to check if
+ // the last rule is removed for a given upstream. If no rule is removed, return early.
+ // Avoid unnecessary work on a non-existent rule which may have never been added or
+ // removed already.
+ if (rules.remove(rule.address) == null) return;
+
+ // Remove the downstream entry if it has no more rule.
+ if (rules.isEmpty()) {
+ mIpv6ForwardingRules.remove(ipServer);
+ }
+
+ // If no more rules between this upstream and downstream, stop upstream forwarding.
+ if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
+ final int downstream = rule.downstreamIfindex;
+ final int upstream = rule.upstreamIfindex;
+ if (!mBpfCoordinatorShim.stopUpstreamIpv6Forwarding(downstream, upstream,
+ rule.srcMac)) {
+ mLog.e("Failed to disable upstream IPv6 forwarding from "
+ + mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream));
+ }
+ }
+
+ // Do cleanup functionality if there is no more rule on the given upstream.
+ maybeClearLimit(rule.upstreamIfindex);
+ }
+
+ /**
+ * Clear all forwarding rules for a given downstream.
+ * Note that this can be only called on handler thread.
+ */
+ public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
+ if (!isUsingBpf()) return;
+
+ final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
+ ipServer);
+ if (rules == null) return;
+
+ // Need to build a rule list because the rule map may be changed in the iteration.
+ for (final Ipv6ForwardingRule rule : new ArrayList<Ipv6ForwardingRule>(rules.values())) {
+ tetherOffloadRuleRemove(ipServer, rule);
+ }
+ }
+
+ /**
+ * Update existing forwarding rules to new upstream for a given downstream.
+ * Note that this can be only called on handler thread.
+ */
+ public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
+ if (!isUsingBpf()) return;
+
+ final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
+ ipServer);
+ if (rules == null) return;
+
+ // Need to build a rule list because the rule map may be changed in the iteration.
+ // First remove all the old rules, then add all the new rules. This is because the upstream
+ // forwarding code in tetherOffloadRuleAdd cannot support rules on two upstreams at the
+ // same time. Deleting the rules first ensures that upstream forwarding is disabled on the
+ // old upstream when the last rule is removed from it, and re-enabled on the new upstream
+ // when the first rule is added to it.
+ // TODO: Once the IPv6 client processing code has moved from IpServer to BpfCoordinator, do
+ // something smarter.
+ final ArrayList<Ipv6ForwardingRule> rulesCopy = new ArrayList<>(rules.values());
+ for (final Ipv6ForwardingRule rule : rulesCopy) {
+ // Remove the old rule before adding the new one because the map uses the same key for
+ // both rules. Reversing the processing order causes that the new rule is removed as
+ // unexpected.
+ // TODO: Add new rule first to reduce the latency which has no rule.
+ tetherOffloadRuleRemove(ipServer, rule);
+ }
+ for (final Ipv6ForwardingRule rule : rulesCopy) {
+ tetherOffloadRuleAdd(ipServer, rule.onNewUpstream(newUpstreamIfindex));
+ }
+ }
+
+ /**
+ * Add upstream name to lookup table. The lookup table is used for tether stats interface name
+ * lookup because the netd only reports interface index in BPF tether stats but the service
+ * expects the interface name in NetworkStats object.
+ * Note that this can be only called on handler thread.
+ */
+ public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) {
+ if (!isUsingBpf()) return;
+
+ if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return;
+
+ // The same interface index to name mapping may be added by different IpServer objects or
+ // re-added by reconnection on the same upstream interface. Ignore the duplicate one.
+ final String iface = mInterfaceNames.get(upstreamIfindex);
+ if (iface == null) {
+ mInterfaceNames.put(upstreamIfindex, upstreamIface);
+ } else if (!TextUtils.equals(iface, upstreamIface)) {
+ Log.wtf(TAG, "The upstream interface name " + upstreamIface
+ + " is different from the existing interface name "
+ + iface + " for index " + upstreamIfindex);
+ }
+ }
+
+ /**
+ * Add downstream client.
+ */
+ public void tetherOffloadClientAdd(@NonNull final IpServer ipServer,
+ @NonNull final ClientInfo client) {
+ if (!isUsingBpf()) return;
+
+ if (!mTetherClients.containsKey(ipServer)) {
+ mTetherClients.put(ipServer, new HashMap<Inet4Address, ClientInfo>());
+ }
+
+ HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+ clients.put(client.clientAddress, client);
+ }
+
+ /**
+ * Remove downstream client.
+ */
+ public void tetherOffloadClientRemove(@NonNull final IpServer ipServer,
+ @NonNull final ClientInfo client) {
+ if (!isUsingBpf()) return;
+
+ HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+ if (clients == null) return;
+
+ // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule
+ // which may have never been added or removed already.
+ if (clients.remove(client.clientAddress) == null) return;
+
+ // Remove the downstream entry if it has no more rule.
+ if (clients.isEmpty()) {
+ mTetherClients.remove(ipServer);
+ }
+ }
+
+ /**
+ * Call when UpstreamNetworkState may be changed.
+ * If upstream has ipv4 for tethering, update this new UpstreamNetworkState to map. The
+ * upstream interface index and its address mapping is prepared for building IPv4
+ * offload rule.
+ *
+ * TODO: Delete the unused upstream interface mapping.
+ * TODO: Support ether ip upstream interface.
+ */
+ public void addUpstreamIfindexToMap(LinkProperties lp) {
+ if (!mPollingStarted) return;
+
+ // This will not work on a network that is using 464xlat because hasIpv4Address will not be
+ // true.
+ // TODO: need to consider 464xlat.
+ if (lp == null || !lp.hasIpv4Address()) return;
+
+ // Support raw ip upstream interface only.
+ final InterfaceParams params = mDeps.getInterfaceParams(lp.getInterfaceName());
+ if (params == null || params.hasMacAddress) return;
+
+ Collection<InetAddress> addresses = lp.getAddresses();
+ for (InetAddress addr: addresses) {
+ if (addr instanceof Inet4Address) {
+ Inet4Address i4addr = (Inet4Address) addr;
+ if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress()
+ && !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) {
+ mIpv4UpstreamIndices.put(i4addr, params.index);
+ }
+ }
+ }
+ }
+
+ /**
+ * Attach BPF program
+ *
+ * TODO: consider error handling if the attach program failed.
+ */
+ public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) {
+ if (forwardingPairExists(intIface, extIface)) return;
+
+ boolean firstDownstreamForThisUpstream = !isAnyForwardingPairOnUpstream(extIface);
+ forwardingPairAdd(intIface, extIface);
+
+ mBpfCoordinatorShim.attachProgram(intIface, UPSTREAM);
+ // Attach if the upstream is the first time to be used in a forwarding pair.
+ if (firstDownstreamForThisUpstream) {
+ mBpfCoordinatorShim.attachProgram(extIface, DOWNSTREAM);
+ }
+ }
+
+ /**
+ * Detach BPF program
+ */
+ public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) {
+ forwardingPairRemove(intIface, extIface);
+
+ // Detaching program may fail because the interface has been removed already.
+ mBpfCoordinatorShim.detachProgram(intIface);
+ // Detach if no more forwarding pair is using the upstream.
+ if (!isAnyForwardingPairOnUpstream(extIface)) {
+ mBpfCoordinatorShim.detachProgram(extIface);
+ }
+ }
+
+ // TODO: make mInterfaceNames accessible to the shim and move this code to there.
+ private String getIfName(long ifindex) {
+ return mInterfaceNames.get((int) ifindex, Long.toString(ifindex));
+ }
+
+ /**
+ * Dump information.
+ * Block the function until all the data are dumped on the handler thread or timed-out. The
+ * reason is that dumpsys invokes this function on the thread of caller and the data may only
+ * be allowed to be accessed on the handler thread.
+ */
+ public void dump(@NonNull IndentingPrintWriter pw) {
+ pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
+ pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
+ pw.println("Stats provider " + (mStatsProvider != null
+ ? "registered" : "not registered"));
+ pw.println("Upstream quota: " + mInterfaceQuotas.toString());
+ pw.println("Polling interval: " + getPollingInterval() + " ms");
+ pw.println("Bpf shim: " + mBpfCoordinatorShim.toString());
+
+ pw.println("Forwarding stats:");
+ pw.increaseIndent();
+ if (mStats.size() == 0) {
+ pw.println("<empty>");
+ } else {
+ dumpStats(pw);
+ }
+ pw.decreaseIndent();
+
+ pw.println("Forwarding rules:");
+ pw.increaseIndent();
+ dumpIpv6UpstreamRules(pw);
+ dumpIpv6ForwardingRules(pw);
+ dumpIpv4ForwardingRules(pw);
+ pw.decreaseIndent();
+
+ pw.println("Device map:");
+ pw.increaseIndent();
+ dumpDevmap(pw);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Forwarding counters:");
+ pw.increaseIndent();
+ dumpCounters(pw);
+ pw.decreaseIndent();
+ }
+
+ private void dumpStats(@NonNull IndentingPrintWriter pw) {
+ for (int i = 0; i < mStats.size(); i++) {
+ final int upstreamIfindex = mStats.keyAt(i);
+ final ForwardedStats stats = mStats.get(upstreamIfindex);
+ pw.println(String.format("%d(%s) - %s", upstreamIfindex, mInterfaceNames.get(
+ upstreamIfindex), stats.toString()));
+ }
+ }
+
+ private void dumpIpv6ForwardingRules(@NonNull IndentingPrintWriter pw) {
+ if (mIpv6ForwardingRules.size() == 0) {
+ pw.println("No IPv6 rules");
+ return;
+ }
+
+ for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>> entry :
+ mIpv6ForwardingRules.entrySet()) {
+ IpServer ipServer = entry.getKey();
+ // The rule downstream interface index is paired with the interface name from
+ // IpServer#interfaceName. See #startIPv6, #updateIpv6ForwardingRules in IpServer.
+ final String downstreamIface = ipServer.interfaceName();
+ pw.println("[" + downstreamIface + "]: iif(iface) oif(iface) v6addr srcmac dstmac");
+
+ pw.increaseIndent();
+ LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = entry.getValue();
+ for (Ipv6ForwardingRule rule : rules.values()) {
+ final int upstreamIfindex = rule.upstreamIfindex;
+ pw.println(String.format("%d(%s) %d(%s) %s %s %s", upstreamIfindex,
+ mInterfaceNames.get(upstreamIfindex), rule.downstreamIfindex,
+ downstreamIface, rule.address.getHostAddress(), rule.srcMac, rule.dstMac));
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ private String ipv6UpstreamRuletoString(TetherUpstream6Key key, Tether6Value value) {
+ return String.format("%d(%s) %s -> %d(%s) %04x %s %s",
+ key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif),
+ value.ethProto, value.ethSrcMac, value.ethDstMac);
+ }
+
+ private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
+ try (BpfMap<TetherUpstream6Key, Tether6Value> map = mDeps.getBpfUpstream6Map()) {
+ if (map == null) {
+ pw.println("No IPv6 upstream");
+ return;
+ }
+ if (map.isEmpty()) {
+ pw.println("No IPv6 upstream rules");
+ return;
+ }
+ map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
+ } catch (ErrnoException e) {
+ pw.println("Error dumping IPv6 upstream map: " + e);
+ }
+ }
+
+ private String ipv4RuleToString(Tether4Key key, Tether4Value value) {
+ final String private4, public4, dst4;
+ try {
+ private4 = InetAddress.getByAddress(key.src4).getHostAddress();
+ dst4 = InetAddress.getByAddress(key.dst4).getHostAddress();
+ public4 = InetAddress.getByAddress(value.src46).getHostAddress();
+ } catch (UnknownHostException impossible) {
+ throw new AssertionError("4-byte array not valid IPv4 address!");
+ }
+ return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d",
+ key.dstMac, key.iif, getIfName(key.iif), private4, key.srcPort,
+ value.oif, getIfName(value.oif),
+ public4, value.srcPort, dst4, key.dstPort);
+ }
+
+ private void dumpIpv4ForwardingRules(IndentingPrintWriter pw) {
+ try (BpfMap<Tether4Key, Tether4Value> map = mDeps.getBpfUpstream4Map()) {
+ if (map == null) {
+ pw.println("No IPv4 support");
+ return;
+ }
+ if (map.isEmpty()) {
+ pw.println("No IPv4 rules");
+ return;
+ }
+ pw.println("IPv4: [inDstMac] iif(iface) src -> nat -> dst");
+ pw.increaseIndent();
+ map.forEach((k, v) -> pw.println(ipv4RuleToString(k, v)));
+ } catch (ErrnoException e) {
+ pw.println("Error dumping IPv4 map: " + e);
+ }
+ pw.decreaseIndent();
+ }
+
+ /**
+ * Simple struct that only contains a u32. Must be public because Struct needs access to it.
+ * TODO: make this a public inner class of Struct so anyone can use it as, e.g., Struct.U32?
+ */
+ public static class U32Struct extends Struct {
+ @Struct.Field(order = 0, type = Struct.Type.U32)
+ public long val;
+ }
+
+ private void dumpCounters(@NonNull IndentingPrintWriter pw) {
+ if (!mDeps.isAtLeastS()) {
+ pw.println("No counter support");
+ return;
+ }
+ try (BpfMap<U32Struct, U32Struct> map = new BpfMap<>(TETHER_ERROR_MAP_PATH,
+ BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) {
+
+ map.forEach((k, v) -> {
+ String counterName;
+ try {
+ counterName = sBpfCounterNames[(int) k.val];
+ } catch (IndexOutOfBoundsException e) {
+ // Should never happen because this code gets the counter name from the same
+ // include file as the BPF program that increments the counter.
+ Log.wtf(TAG, "Unknown tethering counter type " + k.val);
+ counterName = Long.toString(k.val);
+ }
+ if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
+ });
+ } catch (ErrnoException e) {
+ pw.println("Error dumping counter map: " + e);
+ }
+ }
+
+ private void dumpDevmap(@NonNull IndentingPrintWriter pw) {
+ try (BpfMap<TetherDevKey, TetherDevValue> map = mDeps.getBpfDevMap()) {
+ if (map == null) {
+ pw.println("No devmap support");
+ return;
+ }
+ if (map.isEmpty()) {
+ pw.println("No interface index");
+ return;
+ }
+ pw.println("ifindex (iface) -> ifindex (iface)");
+ pw.increaseIndent();
+ map.forEach((k, v) -> {
+ // Only get upstream interface name. Just do the best to make the index readable.
+ // TODO: get downstream interface name because the index is either upstrema or
+ // downstream interface in dev map.
+ pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
+ v.ifIndex, getIfName(v.ifIndex)));
+ });
+ } catch (ErrnoException e) {
+ pw.println("Error dumping dev map: " + e);
+ }
+ pw.decreaseIndent();
+ }
+
+ /** IPv6 forwarding rule class. */
+ public static class Ipv6ForwardingRule {
+ // The upstream6 and downstream6 rules are built as the following tables. Only raw ip
+ // upstream interface is supported.
+ // TODO: support ether ip upstream interface.
+ //
+ // NAT network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ //
+ // upstream6 key and value:
+ //
+ // +------+-------------+
+ // | TetherUpstream6Key |
+ // +------+------+------+
+ // |field |iif |dstMac|
+ // | | | |
+ // +------+------+------+
+ // |value |downst|downst|
+ // | |ream |ream |
+ // +------+------+------+
+ //
+ // +------+----------------------------------+
+ // | |Tether6Value |
+ // +------+------+------+------+------+------+
+ // |field |oif |ethDst|ethSrc|ethPro|pmtu |
+ // | | |mac |mac |to | |
+ // +------+------+------+------+------+------+
+ // |value |upstre|-- |-- |ETH_P_|1500 |
+ // | |am | | |IP | |
+ // +------+------+------+------+------+------+
+ //
+ // downstream6 key and value:
+ //
+ // +------+--------------------+
+ // | |TetherDownstream6Key|
+ // +------+------+------+------+
+ // |field |iif |dstMac|neigh6|
+ // | | | | |
+ // +------+------+------+------+
+ // |value |upstre|-- |client|
+ // | |am | | |
+ // +------+------+------+------+
+ //
+ // +------+----------------------------------+
+ // | |Tether6Value |
+ // +------+------+------+------+------+------+
+ // |field |oif |ethDst|ethSrc|ethPro|pmtu |
+ // | | |mac |mac |to | |
+ // +------+------+------+------+------+------+
+ // |value |downst|client|downst|ETH_P_|1500 |
+ // | |ream | |ream |IP | |
+ // +------+------+------+------+------+------+
+ //
+ public final int upstreamIfindex;
+ public final int downstreamIfindex;
+
+ // TODO: store a ClientInfo object instead of storing address, srcMac, and dstMac directly.
+ @NonNull
+ public final Inet6Address address;
+ @NonNull
+ public final MacAddress srcMac;
+ @NonNull
+ public final MacAddress dstMac;
+
+ public Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex,
+ @NonNull Inet6Address address, @NonNull MacAddress srcMac,
+ @NonNull MacAddress dstMac) {
+ this.upstreamIfindex = upstreamIfindex;
+ this.downstreamIfindex = downstreamIfIndex;
+ this.address = address;
+ this.srcMac = srcMac;
+ this.dstMac = dstMac;
+ }
+
+ /** Return a new rule object which updates with new upstream index. */
+ @NonNull
+ public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) {
+ return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
+ dstMac);
+ }
+
+ /**
+ * Don't manipulate TetherOffloadRuleParcel directly because implementing onNewUpstream()
+ * would be error-prone due to generated stable AIDL classes not having a copy constructor.
+ */
+ @NonNull
+ public TetherOffloadRuleParcel toTetherOffloadRuleParcel() {
+ final TetherOffloadRuleParcel parcel = new TetherOffloadRuleParcel();
+ parcel.inputInterfaceIndex = upstreamIfindex;
+ parcel.outputInterfaceIndex = downstreamIfindex;
+ parcel.destination = address.getAddress();
+ parcel.prefixLength = 128;
+ parcel.srcL2Address = srcMac.toByteArray();
+ parcel.dstL2Address = dstMac.toByteArray();
+ return parcel;
+ }
+
+ /**
+ * Return a TetherDownstream6Key object built from the rule.
+ */
+ @NonNull
+ public TetherDownstream6Key makeTetherDownstream6Key() {
+ return new TetherDownstream6Key(upstreamIfindex, NULL_MAC_ADDRESS,
+ address.getAddress());
+ }
+
+ /**
+ * Return a Tether6Value object built from the rule.
+ */
+ @NonNull
+ public Tether6Value makeTether6Value() {
+ return new Tether6Value(downstreamIfindex, dstMac, srcMac, ETH_P_IPV6,
+ NetworkStackConstants.ETHER_MTU);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Ipv6ForwardingRule)) return false;
+ Ipv6ForwardingRule that = (Ipv6ForwardingRule) o;
+ return this.upstreamIfindex == that.upstreamIfindex
+ && this.downstreamIfindex == that.downstreamIfindex
+ && Objects.equals(this.address, that.address)
+ && Objects.equals(this.srcMac, that.srcMac)
+ && Objects.equals(this.dstMac, that.dstMac);
+ }
+
+ @Override
+ public int hashCode() {
+ // TODO: if this is ever used in production code, don't pass ifindices
+ // to Objects.hash() to avoid autoboxing overhead.
+ return Objects.hash(upstreamIfindex, downstreamIfindex, address, srcMac, dstMac);
+ }
+ }
+
+ /** Tethering client information class. */
+ public static class ClientInfo {
+ public final int downstreamIfindex;
+
+ @NonNull
+ public final MacAddress downstreamMac;
+ @NonNull
+ public final Inet4Address clientAddress;
+ @NonNull
+ public final MacAddress clientMac;
+
+ public ClientInfo(int downstreamIfindex,
+ @NonNull MacAddress downstreamMac, @NonNull Inet4Address clientAddress,
+ @NonNull MacAddress clientMac) {
+ this.downstreamIfindex = downstreamIfindex;
+ this.downstreamMac = downstreamMac;
+ this.clientAddress = clientAddress;
+ this.clientMac = clientMac;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ClientInfo)) return false;
+ ClientInfo that = (ClientInfo) o;
+ return this.downstreamIfindex == that.downstreamIfindex
+ && Objects.equals(this.downstreamMac, that.downstreamMac)
+ && Objects.equals(this.clientAddress, that.clientAddress)
+ && Objects.equals(this.clientMac, that.clientMac);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(downstreamIfindex, downstreamMac, clientAddress, clientMac);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("downstream: %d (%s), client: %s (%s)",
+ downstreamIfindex, downstreamMac, clientAddress, clientMac);
+ }
+ }
+
+ /**
+ * A BPF tethering stats provider to provide network statistics to the system.
+ * Note that this class' data may only be accessed on the handler thread.
+ */
+ @VisibleForTesting
+ class BpfTetherStatsProvider extends NetworkStatsProvider {
+ // The offloaded traffic statistics per interface that has not been reported since the
+ // last call to pushTetherStats. Only the interfaces that were ever tethering upstreams
+ // and has pending tether stats delta are included in this NetworkStats object.
+ private NetworkStats mIfaceStats = new NetworkStats(0L, 0);
+
+ // The same stats as above, but counts network stats per uid.
+ private NetworkStats mUidStats = new NetworkStats(0L, 0);
+
+ @Override
+ public void onRequestStatsUpdate(int token) {
+ mHandler.post(() -> pushTetherStats());
+ }
+
+ @Override
+ public void onSetAlert(long quotaBytes) {
+ mHandler.post(() -> updateAlertQuota(quotaBytes));
+ }
+
+ @Override
+ public void onSetLimit(@NonNull String iface, long quotaBytes) {
+ if (quotaBytes < QUOTA_UNLIMITED) {
+ throw new IllegalArgumentException("invalid quota value " + quotaBytes);
+ }
+
+ mHandler.post(() -> {
+ final Long curIfaceQuota = mInterfaceQuotas.get(iface);
+
+ if (null == curIfaceQuota && QUOTA_UNLIMITED == quotaBytes) return;
+
+ if (quotaBytes == QUOTA_UNLIMITED) {
+ mInterfaceQuotas.remove(iface);
+ } else {
+ mInterfaceQuotas.put(iface, quotaBytes);
+ }
+ maybeUpdateDataLimit(iface);
+ });
+ }
+
+ @VisibleForTesting
+ void pushTetherStats() {
+ try {
+ // The token is not used for now. See b/153606961.
+ notifyStatsUpdated(0 /* token */, mIfaceStats, mUidStats);
+
+ // Clear the accumulated tether stats delta after reported. Note that create a new
+ // empty object because NetworkStats#clear is @hide.
+ mIfaceStats = new NetworkStats(0L, 0);
+ mUidStats = new NetworkStats(0L, 0);
+ } catch (RuntimeException e) {
+ mLog.e("Cannot report network stats: ", e);
+ }
+ }
+
+ private void accumulateDiff(@NonNull NetworkStats ifaceDiff,
+ @NonNull NetworkStats uidDiff) {
+ mIfaceStats = mIfaceStats.add(ifaceDiff);
+ mUidStats = mUidStats.add(uidDiff);
+ }
+ }
+
+ @Nullable
+ private ClientInfo getClientInfo(@NonNull Inet4Address clientAddress) {
+ for (HashMap<Inet4Address, ClientInfo> clients : mTetherClients.values()) {
+ for (ClientInfo client : clients.values()) {
+ if (clientAddress.equals(client.clientAddress)) {
+ return client;
+ }
+ }
+ }
+ return null;
+ }
+
+ // Support raw ip only.
+ // TODO: add ether ip support.
+ // TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules
+ // while TCP status is established.
+ @VisibleForTesting
+ class BpfConntrackEventConsumer implements ConntrackEventConsumer {
+ @NonNull
+ private Tether4Key makeTetherUpstream4Key(
+ @NonNull ConntrackEvent e, @NonNull ClientInfo c) {
+ return new Tether4Key(c.downstreamIfindex, c.downstreamMac,
+ e.tupleOrig.protoNum, e.tupleOrig.srcIp.getAddress(),
+ e.tupleOrig.dstIp.getAddress(), e.tupleOrig.srcPort, e.tupleOrig.dstPort);
+ }
+
+ @NonNull
+ private Tether4Key makeTetherDownstream4Key(
+ @NonNull ConntrackEvent e, @NonNull ClientInfo c, int upstreamIndex) {
+ return new Tether4Key(upstreamIndex, NULL_MAC_ADDRESS /* dstMac (rawip) */,
+ e.tupleReply.protoNum, e.tupleReply.srcIp.getAddress(),
+ e.tupleReply.dstIp.getAddress(), e.tupleReply.srcPort, e.tupleReply.dstPort);
+ }
+
+ @NonNull
+ private Tether4Value makeTetherUpstream4Value(@NonNull ConntrackEvent e,
+ int upstreamIndex) {
+ return new Tether4Value(upstreamIndex,
+ NULL_MAC_ADDRESS /* ethDstMac (rawip) */,
+ NULL_MAC_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
+ NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(e.tupleReply.dstIp),
+ toIpv4MappedAddressBytes(e.tupleReply.srcIp), e.tupleReply.dstPort,
+ e.tupleReply.srcPort, 0 /* lastUsed, filled by bpf prog only */);
+ }
+
+ @NonNull
+ private Tether4Value makeTetherDownstream4Value(@NonNull ConntrackEvent e,
+ @NonNull ClientInfo c, int upstreamIndex) {
+ return new Tether4Value(c.downstreamIfindex,
+ c.clientMac, c.downstreamMac, ETH_P_IP, NetworkStackConstants.ETHER_MTU,
+ toIpv4MappedAddressBytes(e.tupleOrig.dstIp),
+ toIpv4MappedAddressBytes(e.tupleOrig.srcIp),
+ e.tupleOrig.dstPort, e.tupleOrig.srcPort,
+ 0 /* lastUsed, filled by bpf prog only */);
+ }
+
+ @NonNull
+ private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
+ final byte[] addr4 = ia4.getAddress();
+ final byte[] addr6 = new byte[16];
+ addr6[10] = (byte) 0xff;
+ addr6[11] = (byte) 0xff;
+ addr6[12] = addr4[0];
+ addr6[13] = addr4[1];
+ addr6[14] = addr4[2];
+ addr6[15] = addr4[3];
+ return addr6;
+ }
+
+ public void accept(ConntrackEvent e) {
+ final ClientInfo tetherClient = getClientInfo(e.tupleOrig.srcIp);
+ if (tetherClient == null) return;
+
+ final Integer upstreamIndex = mIpv4UpstreamIndices.get(e.tupleReply.dstIp);
+ if (upstreamIndex == null) return;
+
+ final Tether4Key upstream4Key = makeTetherUpstream4Key(e, tetherClient);
+ final Tether4Key downstream4Key = makeTetherDownstream4Key(e, tetherClient,
+ upstreamIndex);
+
+ if (e.msgType == (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+ | NetlinkConstants.IPCTNL_MSG_CT_DELETE)) {
+ mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, upstream4Key);
+ mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, downstream4Key);
+ maybeClearLimit(upstreamIndex);
+ return;
+ }
+
+ final Tether4Value upstream4Value = makeTetherUpstream4Value(e, upstreamIndex);
+ final Tether4Value downstream4Value = makeTetherDownstream4Value(e, tetherClient,
+ upstreamIndex);
+
+ maybeAddDevMap(upstreamIndex, tetherClient.downstreamIfindex);
+ maybeSetLimit(upstreamIndex);
+ mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value);
+ mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value);
+ }
+ }
+
+ private boolean isBpfEnabled() {
+ final TetheringConfiguration config = mDeps.getTetherConfig();
+ return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */;
+ }
+
+ private int getInterfaceIndexFromRules(@NonNull String ifName) {
+ for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
+ .values()) {
+ for (Ipv6ForwardingRule rule : rules.values()) {
+ final int upstreamIfindex = rule.upstreamIfindex;
+ if (TextUtils.equals(ifName, mInterfaceNames.get(upstreamIfindex))) {
+ return upstreamIfindex;
+ }
+ }
+ }
+ return 0;
+ }
+
+ private long getQuotaBytes(@NonNull String iface) {
+ final Long limit = mInterfaceQuotas.get(iface);
+ final long quotaBytes = (limit != null) ? limit : QUOTA_UNLIMITED;
+
+ return quotaBytes;
+ }
+
+ private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) {
+ if (ifIndex == 0) {
+ Log.wtf(TAG, "Invalid interface index.");
+ return false;
+ }
+
+ return mBpfCoordinatorShim.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
+ }
+
+ // Handle the data limit update from the service which is the stats provider registered for.
+ private void maybeUpdateDataLimit(@NonNull String iface) {
+ // Set data limit only on a given upstream which has at least one rule. If we can't get
+ // an interface index for a given interface name, it means either there is no rule for
+ // a given upstream or the interface name is not an upstream which is monitored by the
+ // coordinator.
+ final int ifIndex = getInterfaceIndexFromRules(iface);
+ if (ifIndex == 0) return;
+
+ final long quotaBytes = getQuotaBytes(iface);
+ sendDataLimitToBpfMap(ifIndex, quotaBytes);
+ }
+
+ // Handle the data limit update while adding forwarding rules.
+ private boolean updateDataLimit(int ifIndex) {
+ final String iface = mInterfaceNames.get(ifIndex);
+ if (iface == null) {
+ mLog.e("Fail to get the interface name for index " + ifIndex);
+ return false;
+ }
+ final long quotaBytes = getQuotaBytes(iface);
+ return sendDataLimitToBpfMap(ifIndex, quotaBytes);
+ }
+
+ private void maybeSetLimit(int upstreamIfindex) {
+ if (isAnyRuleOnUpstream(upstreamIfindex)
+ || mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream(upstreamIfindex)) {
+ return;
+ }
+
+ // If failed to set a data limit, probably should not use this upstream, because
+ // the upstream may not want to blow through the data limit that was told to apply.
+ // TODO: Perhaps stop the coordinator.
+ boolean success = updateDataLimit(upstreamIfindex);
+ if (!success) {
+ final String iface = mInterfaceNames.get(upstreamIfindex);
+ mLog.e("Setting data limit for " + iface + " failed.");
+ }
+ }
+
+ // TODO: This should be also called while IpServer wants to clear all IPv4 rules. Relying on
+ // conntrack event can't cover this case.
+ private void maybeClearLimit(int upstreamIfindex) {
+ if (isAnyRuleOnUpstream(upstreamIfindex)
+ || mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream(upstreamIfindex)) {
+ return;
+ }
+
+ final TetherStatsValue statsValue =
+ mBpfCoordinatorShim.tetherOffloadGetAndClearStats(upstreamIfindex);
+ if (statsValue == null) {
+ Log.wtf(TAG, "Fail to cleanup tether stats for upstream index " + upstreamIfindex);
+ return;
+ }
+
+ SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+ tetherStatsList.put(upstreamIfindex, statsValue);
+
+ // Update the last stats delta and delete the local cache for a given upstream.
+ updateQuotaAndStatsFromSnapshot(tetherStatsList);
+ mStats.remove(upstreamIfindex);
+ }
+
+ // TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called
+ // both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream.
+ private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
+ for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
+ .values()) {
+ for (Ipv6ForwardingRule rule : rules.values()) {
+ if (upstreamIfindex == rule.upstreamIfindex) return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) {
+ for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
+ .values()) {
+ for (Ipv6ForwardingRule rule : rules.values()) {
+ if (downstreamIfindex == rule.downstreamIfindex
+ && upstreamIfindex == rule.upstreamIfindex) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // TODO: remove the index from map while the interface has been removed because the map size
+ // is 64 entries. See packages\modules\Connectivity\Tethering\bpf_progs\offload.c.
+ private void maybeAddDevMap(int upstreamIfindex, int downstreamIfindex) {
+ for (Integer index : new Integer[] {upstreamIfindex, downstreamIfindex}) {
+ if (mDeviceMapSet.contains(index)) continue;
+ if (mBpfCoordinatorShim.addDevMap(index)) mDeviceMapSet.add(index);
+ }
+ }
+
+ private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) {
+ if (!mForwardingPairs.containsKey(extIface)) {
+ mForwardingPairs.put(extIface, new HashSet<String>());
+ }
+ mForwardingPairs.get(extIface).add(intIface);
+ }
+
+ private void forwardingPairRemove(@NonNull String intIface, @NonNull String extIface) {
+ HashSet<String> downstreams = mForwardingPairs.get(extIface);
+ if (downstreams == null) return;
+ if (!downstreams.remove(intIface)) return;
+
+ if (downstreams.isEmpty()) {
+ mForwardingPairs.remove(extIface);
+ }
+ }
+
+ private boolean forwardingPairExists(@NonNull String intIface, @NonNull String extIface) {
+ if (!mForwardingPairs.containsKey(extIface)) return false;
+
+ return mForwardingPairs.get(extIface).contains(intIface);
+ }
+
+ private boolean isAnyForwardingPairOnUpstream(@NonNull String extIface) {
+ return mForwardingPairs.containsKey(extIface);
+ }
+
+ @NonNull
+ private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex,
+ @NonNull final ForwardedStats diff) {
+ NetworkStats stats = new NetworkStats(0L, 0);
+ final String iface = mInterfaceNames.get(ifIndex);
+ if (iface == null) {
+ // TODO: Use Log.wtf once the coordinator owns full control of tether stats from netd.
+ // For now, netd may add the empty stats for the upstream which is not monitored by
+ // the coordinator. Silently ignore it.
+ return stats;
+ }
+ final int uid = (type == StatsType.STATS_PER_UID) ? UID_TETHERING : UID_ALL;
+ // Note that the argument 'metered', 'roaming' and 'defaultNetwork' are not recorded for
+ // network stats snapshot. See NetworkStatsRecorder#recordSnapshotLocked.
+ return stats.addEntry(new Entry(iface, uid, SET_DEFAULT, TAG_NONE, METERED_NO,
+ ROAMING_NO, DEFAULT_NETWORK_NO, diff.rxBytes, diff.rxPackets,
+ diff.txBytes, diff.txPackets, 0L /* operations */));
+ }
+
+ private void updateAlertQuota(long newQuota) {
+ if (newQuota < QUOTA_UNLIMITED) {
+ throw new IllegalArgumentException("invalid quota value " + newQuota);
+ }
+ if (mRemainingAlertQuota == newQuota) return;
+
+ mRemainingAlertQuota = newQuota;
+ if (mRemainingAlertQuota == 0) {
+ mLog.i("onAlertReached");
+ if (mStatsProvider != null) mStatsProvider.notifyAlertReached();
+ }
+ }
+
+ private void updateQuotaAndStatsFromSnapshot(
+ @NonNull final SparseArray<TetherStatsValue> tetherStatsList) {
+ long usedAlertQuota = 0;
+ for (int i = 0; i < tetherStatsList.size(); i++) {
+ final Integer ifIndex = tetherStatsList.keyAt(i);
+ final TetherStatsValue tetherStats = tetherStatsList.valueAt(i);
+ final ForwardedStats curr = new ForwardedStats(tetherStats);
+ final ForwardedStats base = mStats.get(ifIndex);
+ final ForwardedStats diff = (base != null) ? curr.subtract(base) : curr;
+ usedAlertQuota += diff.rxBytes + diff.txBytes;
+
+ // Update the local cache for counting tether stats delta.
+ mStats.put(ifIndex, curr);
+
+ // Update the accumulated tether stats delta to the stats provider for the service
+ // querying.
+ if (mStatsProvider != null) {
+ try {
+ mStatsProvider.accumulateDiff(
+ buildNetworkStats(StatsType.STATS_PER_IFACE, ifIndex, diff),
+ buildNetworkStats(StatsType.STATS_PER_UID, ifIndex, diff));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ Log.wtf(TAG, "Fail to update the accumulated stats delta for interface index "
+ + ifIndex + " : ", e);
+ }
+ }
+ }
+
+ if (mRemainingAlertQuota > 0 && usedAlertQuota > 0) {
+ // Trim to zero if overshoot.
+ final long newQuota = Math.max(mRemainingAlertQuota - usedAlertQuota, 0);
+ updateAlertQuota(newQuota);
+ }
+
+ // TODO: Count the used limit quota for notifying data limit reached.
+ }
+
+ private void updateForwardedStats() {
+ final SparseArray<TetherStatsValue> tetherStatsList =
+ mBpfCoordinatorShim.tetherOffloadGetStats();
+
+ if (tetherStatsList == null) {
+ mLog.e("Problem fetching tethering stats");
+ return;
+ }
+
+ updateQuotaAndStatsFromSnapshot(tetherStatsList);
+ }
+
+ @VisibleForTesting
+ int getPollingInterval() {
+ // The valid range of interval is DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long.
+ // Ignore the config value is less than the minimum polling interval. Note that the
+ // minimum interval definition is invoked as OffloadController#isPollingStatsNeeded does.
+ // TODO: Perhaps define a minimum polling interval constant.
+ final TetheringConfiguration config = mDeps.getTetherConfig();
+ final int configInterval = (config != null) ? config.getOffloadPollInterval() : 0;
+ return Math.max(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, configInterval);
+ }
+
+ private void maybeSchedulePollingStats() {
+ if (!mPollingStarted) return;
+
+ if (mHandler.hasCallbacks(mScheduledPollingTask)) {
+ mHandler.removeCallbacks(mScheduledPollingTask);
+ }
+
+ mHandler.postDelayed(mScheduledPollingTask, getPollingInterval());
+ }
+
+ // Return forwarding rule map. This is used for testing only.
+ // Note that this can be only called on handler thread.
+ @NonNull
+ @VisibleForTesting
+ final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
+ getForwardingRulesForTesting() {
+ return mIpv6ForwardingRules;
+ }
+
+ // Return upstream interface name map. This is used for testing only.
+ // Note that this can be only called on handler thread.
+ @NonNull
+ @VisibleForTesting
+ final SparseArray<String> getInterfaceNamesForTesting() {
+ return mInterfaceNames;
+ }
+
+ // Return BPF conntrack event consumer. This is used for testing only.
+ // Note that this can be only called on handler thread.
+ @NonNull
+ @VisibleForTesting
+ final BpfConntrackEventConsumer getBpfConntrackEventConsumerForTesting() {
+ return mBpfConntrackEventConsumer;
+ }
+
+ private static native String[] getBpfCounterNames();
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
new file mode 100644
index 0000000..1363dc5
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
@@ -0,0 +1,288 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.Struct;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+/**
+ * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
+ * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by
+ * passing syscalls with map file descriptor.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable {
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ // Following definitions from kernel include/uapi/linux/bpf.h
+ public static final int BPF_F_RDWR = 0;
+ public static final int BPF_F_RDONLY = 1 << 3;
+ public static final int BPF_F_WRONLY = 1 << 4;
+
+ public static final int BPF_MAP_TYPE_HASH = 1;
+
+ private static final int BPF_F_NO_PREALLOC = 1;
+
+ private static final int BPF_ANY = 0;
+ private static final int BPF_NOEXIST = 1;
+ private static final int BPF_EXIST = 2;
+
+ private final int mMapFd;
+ private final Class<K> mKeyClass;
+ private final Class<V> mValueClass;
+ private final int mKeySize;
+ private final int mValueSize;
+
+ /**
+ * Create a BpfMap map wrapper with "path" of filesystem.
+ *
+ * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY.
+ * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+ * @throws NullPointerException if {@code path} is null.
+ */
+ public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
+ final Class<V> value) throws ErrnoException, NullPointerException {
+ mMapFd = bpfFdGet(path, flag);
+
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
+ /**
+ * Constructor for testing only.
+ * The derived class implements an internal mocked map. It need to implement all functions
+ * which are related with the native BPF map because the BPF map handler is not initialized.
+ * See BpfCoordinatorTest#TestBpfMap.
+ */
+ @VisibleForTesting
+ protected BpfMap(final Class<K> key, final Class<V> value) {
+ mMapFd = -1;
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
+ /**
+ * Update an existing or create a new key -> value entry in an eBbpf map.
+ * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
+ */
+ public void updateEntry(K key, V value) throws ErrnoException {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+ }
+
+ /**
+ * If the key does not exist in the map, insert key -> value entry into eBpf map.
+ * Otherwise IllegalStateException will be thrown.
+ */
+ public void insertEntry(K key, V value)
+ throws ErrnoException, IllegalStateException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+ } catch (ErrnoException e) {
+ if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
+
+ throw e;
+ }
+ }
+
+ /**
+ * If the key already exists in the map, replace its value. Otherwise NoSuchElementException
+ * will be thrown.
+ */
+ public void replaceEntry(K key, V value)
+ throws ErrnoException, NoSuchElementException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+ } catch (ErrnoException e) {
+ if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
+
+ throw e;
+ }
+ }
+
+ /**
+ * Update an existing or create a new key -> value entry in an eBbpf map.
+ * Returns true if inserted, false if replaced.
+ * (use updateEntry() if you don't care whether insert or replace happened)
+ * Note: see inline comment below if running concurrently with delete operations.
+ */
+ public boolean insertOrReplaceEntry(K key, V value)
+ throws ErrnoException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+ return true; /* insert succeeded */
+ } catch (ErrnoException e) {
+ if (e.errno != EEXIST) throw e;
+ }
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+ return false; /* replace succeeded */
+ } catch (ErrnoException e) {
+ if (e.errno != ENOENT) throw e;
+ }
+ /* If we reach here somebody deleted after our insert attempt and before our replace:
+ * this implies a race happened. The kernel bpf delete interface only takes a key,
+ * and not the value, so we can safely pretend the replace actually succeeded and
+ * was immediately followed by the other thread's delete, since the delete cannot
+ * observe the potential change to the value.
+ */
+ return false; /* pretend replace succeeded */
+ }
+
+ /** Remove existing key from eBpf map. Return false if map was not modified. */
+ public boolean deleteEntry(K key) throws ErrnoException {
+ return deleteMapEntry(mMapFd, key.writeToBytes());
+ }
+
+ /** Returns {@code true} if this map contains no elements. */
+ public boolean isEmpty() throws ErrnoException {
+ return getFirstKey() == null;
+ }
+
+ private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
+ final byte[] rawKey = getNextRawKey(
+ key == null ? null : key.writeToBytes());
+ if (rawKey == null) return null;
+
+ final ByteBuffer buffer = ByteBuffer.wrap(rawKey);
+ buffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(mKeyClass, buffer);
+ }
+
+ /**
+ * Get the next key of the passed-in key. If the passed-in key is not found, return the first
+ * key. If the passed-in key is the last one, return null.
+ *
+ * TODO: consider allowing null passed-in key.
+ */
+ public K getNextKey(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+ return getNextKeyInternal(key);
+ }
+
+ private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
+ byte[] nextKey = new byte[mKeySize];
+ if (getNextMapKey(mMapFd, key, nextKey)) return nextKey;
+
+ return null;
+ }
+
+ /** Get the first key of eBpf map. */
+ public K getFirstKey() throws ErrnoException {
+ return getNextKeyInternal(null);
+ }
+
+ /** Check whether a key exists in the map. */
+ public boolean containsKey(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+
+ final byte[] rawValue = getRawValue(key.writeToBytes());
+ return rawValue != null;
+ }
+
+ /** Retrieve a value from the map. Return null if there is no such key. */
+ public V getValue(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+ final byte[] rawValue = getRawValue(key.writeToBytes());
+
+ if (rawValue == null) return null;
+
+ final ByteBuffer buffer = ByteBuffer.wrap(rawValue);
+ buffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(mValueClass, buffer);
+ }
+
+ private byte[] getRawValue(final byte[] key) throws ErrnoException {
+ byte[] value = new byte[mValueSize];
+ if (findMapEntry(mMapFd, key, value)) return value;
+
+ return null;
+ }
+
+ /**
+ * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+ * other structural modifications to the map, such as adding entries or deleting other entries.
+ * Otherwise, iteration will result in undefined behaviour.
+ */
+ public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+ @Nullable K nextKey = getFirstKey();
+
+ while (nextKey != null) {
+ @NonNull final K curKey = nextKey;
+ @NonNull final V value = getValue(curKey);
+
+ nextKey = getNextKey(curKey);
+ action.accept(curKey, value);
+ }
+ }
+
+ @Override
+ public void close() throws ErrnoException {
+ closeMap(mMapFd);
+ }
+
+ /**
+ * Clears the map. The map may already be empty.
+ *
+ * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+ * or if a non-ENOENT error occurred when deleting a key.
+ */
+ public void clear() throws ErrnoException {
+ K key = getFirstKey();
+ while (key != null) {
+ deleteEntry(key); // ignores ENOENT.
+ key = getFirstKey();
+ }
+ }
+
+ private static native int closeMap(int fd) throws ErrnoException;
+
+ private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
+
+ private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
+ throws ErrnoException;
+
+ private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException;
+
+ // If key is found, the operation returns true and the nextKey would reference to the next
+ // element. If key is not found, the operation returns true and the nextKey would reference to
+ // the first element. If key is the last element, false is returned.
+ private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException;
+
+ private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException;
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
new file mode 100644
index 0000000..0b44249
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -0,0 +1,144 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+
+import android.net.util.InterfaceParams;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+
+/**
+ * The classes and the methods for BPF utilization.
+ *
+ * {@hide}
+ */
+public class BpfUtils {
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ // For better code clarity when used for 'bool ingress' parameter.
+ static final boolean EGRESS = false;
+ static final boolean INGRESS = true;
+
+ // For better code clarify when used for 'bool downstream' parameter.
+ //
+ // This is talking about the direction of travel of the offloaded packets.
+ //
+ // Upstream means packets heading towards the internet/uplink (upload),
+ // thus for tethering this is attached to ingress on the downstream interface,
+ // while for clat this is attached to egress on the v4-* clat interface.
+ //
+ // Downstream means packets coming from the internet/uplink (download), thus
+ // for both clat and tethering this is attached to ingress on the upstream interface.
+ static final boolean DOWNSTREAM = true;
+ static final boolean UPSTREAM = false;
+
+ // The priority of clat/tether hooks - smaller is higher priority.
+ // TC tether is higher priority then TC clat to match XDP winning over TC.
+ // Sync from system/netd/server/OffloadUtils.h.
+ static final short PRIO_TETHER6 = 1;
+ static final short PRIO_TETHER4 = 2;
+ // note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp
+
+ private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) {
+ String path = "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_"
+ + (downstream ? "downstream" : "upstream")
+ + ipVersion + "_"
+ + (ether ? "ether" : "rawip");
+ return path;
+ }
+
+ /**
+ * Attach BPF program
+ *
+ * TODO: use interface index to replace interface name.
+ */
+ public static void attachProgram(@NonNull String iface, boolean downstream)
+ throws IOException {
+ final InterfaceParams params = InterfaceParams.getByName(iface);
+ if (params == null) {
+ throw new IOException("Fail to get interface params for interface " + iface);
+ }
+
+ boolean ether;
+ try {
+ ether = isEthernet(iface);
+ } catch (IOException e) {
+ throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e);
+ }
+
+ try {
+ // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/...
+ // direct-action
+ tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6,
+ makeProgPath(downstream, 6, ether));
+ } catch (IOException e) {
+ throw new IOException("tc filter add dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e);
+ }
+
+ try {
+ // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/...
+ // direct-action
+ tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP,
+ makeProgPath(downstream, 4, ether));
+ } catch (IOException e) {
+ throw new IOException("tc filter add dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e);
+ }
+ }
+
+ /**
+ * Detach BPF program
+ *
+ * TODO: use interface index to replace interface name.
+ */
+ public static void detachProgram(@NonNull String iface) throws IOException {
+ final InterfaceParams params = InterfaceParams.getByName(iface);
+ if (params == null) {
+ throw new IOException("Fail to get interface params for interface " + iface);
+ }
+
+ try {
+ // tc filter del dev .. ingress prio 1 protocol ipv6
+ tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6);
+ } catch (IOException e) {
+ throw new IOException("tc filter del dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e);
+ }
+
+ try {
+ // tc filter del dev .. ingress prio 2 protocol ip
+ tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP);
+ } catch (IOException e) {
+ throw new IOException("tc filter del dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e);
+ }
+ }
+
+ private static native boolean isEthernet(String iface) throws IOException;
+
+ private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio,
+ short proto, String bpfProgPath) throws IOException;
+
+ private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio,
+ short proto) throws IOException;
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
new file mode 100644
index 0000000..8a96988
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
@@ -0,0 +1,183 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+
+import android.net.MacAddress;
+import android.net.TetheredClient;
+import android.net.TetheredClient.AddressInfo;
+import android.net.ip.IpServer;
+import android.net.wifi.WifiClient;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tracker for clients connected to downstreams.
+ *
+ * <p>This class is not thread safe, it is intended to be used only from the tethering handler
+ * thread.
+ */
+public class ConnectedClientsTracker {
+ private final Clock mClock;
+
+ @NonNull
+ private List<WifiClient> mLastWifiClients = Collections.emptyList();
+ @NonNull
+ private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
+
+ @VisibleForTesting
+ static class Clock {
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+
+ public ConnectedClientsTracker() {
+ this(new Clock());
+ }
+
+ @VisibleForTesting
+ ConnectedClientsTracker(Clock clock) {
+ mClock = clock;
+ }
+
+ /**
+ * Update the tracker with new connected clients.
+ *
+ * <p>The new list can be obtained through {@link #getLastTetheredClients()}.
+ * @param ipServers The IpServers used to assign addresses to clients.
+ * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
+ * update.
+ * @return True if the list of clients changed since the last calculation.
+ */
+ public boolean updateConnectedClients(
+ Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
+ final long now = mClock.elapsedRealtime();
+
+ if (wifiClients != null) {
+ mLastWifiClients = wifiClients;
+ }
+ final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
+
+ // Build the list of non-expired leases from all IpServers, grouped by mac address
+ final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
+ for (IpServer server : ipServers) {
+ for (TetheredClient client : server.getAllLeases()) {
+ if (client.getTetheringType() == TETHERING_WIFI
+ && !wifiClientMacs.contains(client.getMacAddress())) {
+ // Skip leases of WiFi clients that are not (or no longer) L2-connected
+ continue;
+ }
+ final TetheredClient prunedClient = pruneExpired(client, now);
+ if (prunedClient == null) continue; // All addresses expired
+
+ addLease(clientsMap, prunedClient);
+ }
+ }
+
+ // TODO: add IPv6 addresses from netlink
+
+ // Add connected WiFi clients that do not have any known address
+ for (MacAddress client : wifiClientMacs) {
+ if (clientsMap.containsKey(client)) continue;
+ clientsMap.put(client, new TetheredClient(
+ client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
+ }
+
+ final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
+ final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
+ || !clients.containsAll(mLastTetheredClients);
+ mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients));
+ return clientsChanged;
+ }
+
+ private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
+ final TetheredClient aggregateClient = clientsMap.getOrDefault(
+ lease.getMacAddress(), lease);
+ if (aggregateClient == lease) {
+ // This is the first lease with this mac address
+ clientsMap.put(lease.getMacAddress(), lease);
+ return;
+ }
+
+ // Only add the address info; this assumes that the tethering type is the same when the mac
+ // address is the same. If a client is connected through different tethering types with the
+ // same mac address, connected clients callbacks will report all of its addresses under only
+ // one of these tethering types. This keeps the API simple considering that such a scenario
+ // would really be a rare edge case.
+ clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease));
+ }
+
+ /**
+ * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}.
+ *
+ * <p>The returned list is immutable.
+ */
+ @NonNull
+ public List<TetheredClient> getLastTetheredClients() {
+ return mLastTetheredClients;
+ }
+
+ private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) {
+ for (AddressInfo info : addresses) {
+ if (info.getExpirationTime() <= now) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static TetheredClient pruneExpired(TetheredClient client, long now) {
+ final List<AddressInfo> addresses = client.getAddresses();
+ if (addresses.size() == 0) return null;
+ if (!hasExpiredAddress(addresses, now)) return client;
+
+ final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1);
+ for (AddressInfo info : addresses) {
+ if (info.getExpirationTime() > now) {
+ newAddrs.add(info);
+ }
+ }
+
+ if (newAddrs.size() == 0) {
+ return null;
+ }
+ return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType());
+ }
+
+ @NonNull
+ private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) {
+ final Set<MacAddress> macs = new HashSet<>(clients.size());
+ for (WifiClient c : clients) {
+ macs.add(c.getMacAddress());
+ }
+ return macs;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
new file mode 100644
index 0000000..60fcfd0
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -0,0 +1,639 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.TetheringConstants.EXTRA_ADD_TETHER_TYPE;
+import static android.net.TetheringConstants.EXTRA_PROVISION_CALLBACK;
+import static android.net.TetheringConstants.EXTRA_RUN_PROVISION;
+import static android.net.TetheringConstants.EXTRA_TETHER_PROVISIONING_RESPONSE;
+import static android.net.TetheringConstants.EXTRA_TETHER_SILENT_PROVISIONING_ACTION;
+import static android.net.TetheringConstants.EXTRA_TETHER_SUBID;
+import static android.net.TetheringConstants.EXTRA_TETHER_UI_PROVISIONING_APP_NAME;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_INVALID;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.util.SharedLog;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.BitSet;
+
+/**
+ * Re-check tethering provisioning for enabled downstream tether types.
+ * Reference TetheringManager.TETHERING_{@code *} for each tether type.
+ *
+ * All methods of this class must be accessed from the thread of tethering
+ * state machine.
+ * @hide
+ */
+public class EntitlementManager {
+ private static final String TAG = EntitlementManager.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ @VisibleForTesting
+ protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning";
+ private static final String ACTION_PROVISIONING_ALARM =
+ "com.android.networkstack.tethering.PROVISIONING_RECHECK_ALARM";
+
+ private final ComponentName mSilentProvisioningService;
+ private static final int MS_PER_HOUR = 60 * 60 * 1000;
+ private static final int DUMP_TIMEOUT = 10_000;
+
+ // The BitSet is the bit map of each enabled downstream types, ex:
+ // {@link TetheringManager.TETHERING_WIFI}
+ // {@link TetheringManager.TETHERING_USB}
+ // {@link TetheringManager.TETHERING_BLUETOOTH}
+ private final BitSet mCurrentDownstreams;
+ private final BitSet mExemptedDownstreams;
+ private final Context mContext;
+ private final SharedLog mLog;
+ private final SparseIntArray mEntitlementCacheValue;
+ private final Handler mHandler;
+ // Key: TetheringManager.TETHERING_*(downstream).
+ // Value: TetheringManager.TETHER_ERROR_{NO_ERROR or PROVISION_FAILED}(provisioning result).
+ private final SparseIntArray mCurrentEntitlementResults;
+ private final Runnable mPermissionChangeCallback;
+ private PendingIntent mProvisioningRecheckAlarm;
+ private boolean mLastCellularUpstreamPermitted = true;
+ private boolean mUsingCellularAsUpstream = false;
+ private boolean mNeedReRunProvisioningUi = false;
+ private OnUiEntitlementFailedListener mListener;
+ private TetheringConfigurationFetcher mFetcher;
+
+ public EntitlementManager(Context ctx, Handler h, SharedLog log,
+ Runnable callback) {
+ mContext = ctx;
+ mLog = log.forSubComponent(TAG);
+ mCurrentDownstreams = new BitSet();
+ mExemptedDownstreams = new BitSet();
+ mCurrentEntitlementResults = new SparseIntArray();
+ mEntitlementCacheValue = new SparseIntArray();
+ mPermissionChangeCallback = callback;
+ mHandler = h;
+ mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
+ null, mHandler);
+ mSilentProvisioningService = ComponentName.unflattenFromString(
+ mContext.getResources().getString(R.string.config_wifi_tether_enable));
+ }
+
+ public void setOnUiEntitlementFailedListener(final OnUiEntitlementFailedListener listener) {
+ mListener = listener;
+ }
+
+ /** Callback fired when UI entitlement failed. */
+ public interface OnUiEntitlementFailedListener {
+ /**
+ * Ui entitlement check fails in |downstream|.
+ *
+ * @param downstream tethering type from TetheringManager.TETHERING_{@code *}.
+ */
+ void onUiEntitlementFailed(int downstream);
+ }
+
+ public void setTetheringConfigurationFetcher(final TetheringConfigurationFetcher fetcher) {
+ mFetcher = fetcher;
+ }
+
+ /** Interface to fetch TetheringConfiguration. */
+ public interface TetheringConfigurationFetcher {
+ /**
+ * Fetch current tethering configuration. This will be called to ensure whether entitlement
+ * check is needed.
+ * @return TetheringConfiguration instance.
+ */
+ TetheringConfiguration fetchTetheringConfiguration();
+ }
+
+ /**
+ * Check if cellular upstream is permitted.
+ */
+ public boolean isCellularUpstreamPermitted() {
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+
+ return isCellularUpstreamPermitted(config);
+ }
+
+ private boolean isCellularUpstreamPermitted(final TetheringConfiguration config) {
+ if (!isTetherProvisioningRequired(config)) return true;
+
+ // If provisioning is required and EntitlementManager doesn't know any downstreams, cellular
+ // upstream should not be enabled. Enable cellular upstream for exempted downstreams only
+ // when there is no non-exempted downstream.
+ if (mCurrentDownstreams.isEmpty()) return !mExemptedDownstreams.isEmpty();
+
+ return mCurrentEntitlementResults.indexOfValue(TETHER_ERROR_NO_ERROR) > -1;
+ }
+
+ /**
+ * Set exempted downstream type. If there is only exempted downstream type active,
+ * corresponding entitlement check will not be run and cellular upstream will be permitted
+ * by default. If a privileged app enables tethering without a provisioning check, and then
+ * another app enables tethering of the same type but does not disable the provisioning check,
+ * then the downstream immediately loses exempt status and a provisioning check is run.
+ * If any non-exempted downstream type is active, the cellular upstream will be gated by the
+ * result of entitlement check from non-exempted downstreams. If entitlement check is still
+ * in progress on non-exempt downstreams, ceullar upstream would default be disabled. When any
+ * non-exempted downstream gets positive entitlement result, ceullar upstream will be enabled.
+ */
+ public void setExemptedDownstreamType(final int type) {
+ mExemptedDownstreams.set(type, true);
+ }
+
+ /**
+ * This is called when tethering starts.
+ * Launch provisioning app if upstream is cellular.
+ *
+ * @param downstreamType tethering type from TetheringManager.TETHERING_{@code *}
+ * @param showProvisioningUi a boolean indicating whether to show the
+ * provisioning app UI if there is one.
+ */
+ public void startProvisioningIfNeeded(int downstreamType, boolean showProvisioningUi) {
+ if (!isValidDownstreamType(downstreamType)) return;
+
+ mCurrentDownstreams.set(downstreamType, true);
+
+ mExemptedDownstreams.set(downstreamType, false);
+
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ if (!isTetherProvisioningRequired(config)) return;
+
+ // If upstream is not cellular, provisioning app would not be launched
+ // till upstream change to cellular.
+ if (mUsingCellularAsUpstream) {
+ if (showProvisioningUi) {
+ runUiTetherProvisioning(downstreamType, config);
+ } else {
+ runSilentTetherProvisioning(downstreamType, config);
+ }
+ mNeedReRunProvisioningUi = false;
+ } else {
+ mNeedReRunProvisioningUi |= showProvisioningUi;
+ }
+ }
+
+ /**
+ * Tell EntitlementManager that a given type of tethering has been disabled
+ *
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ */
+ public void stopProvisioningIfNeeded(int downstreamType) {
+ if (!isValidDownstreamType(downstreamType)) return;
+
+ mCurrentDownstreams.set(downstreamType, false);
+ // There are lurking bugs where the notion of "provisioning required" or
+ // "tethering supported" may change without without tethering being notified properly.
+ // Remove the mapping all the time no matter provisioning is required or not.
+ removeDownstreamMapping(downstreamType);
+ mExemptedDownstreams.set(downstreamType, false);
+ }
+
+ /**
+ * Notify EntitlementManager if upstream is cellular or not.
+ *
+ * @param isCellular whether tethering upstream is cellular.
+ */
+ public void notifyUpstream(boolean isCellular) {
+ if (DBG) {
+ mLog.i("notifyUpstream: " + isCellular
+ + ", mLastCellularUpstreamPermitted: " + mLastCellularUpstreamPermitted
+ + ", mNeedReRunProvisioningUi: " + mNeedReRunProvisioningUi);
+ }
+ mUsingCellularAsUpstream = isCellular;
+
+ if (mUsingCellularAsUpstream) {
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ maybeRunProvisioning(config);
+ }
+ }
+
+ /** Run provisioning if needed */
+ public void maybeRunProvisioning() {
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ maybeRunProvisioning(config);
+ }
+
+ private void maybeRunProvisioning(final TetheringConfiguration config) {
+ if (mCurrentDownstreams.isEmpty() || !isTetherProvisioningRequired(config)) {
+ return;
+ }
+
+ // Whenever any entitlement value changes, all downstreams will re-evaluate whether they
+ // are allowed. Therefore even if the silent check here ends in a failure and the UI later
+ // yields success, then the downstream that got a failure will re-evaluate as a result of
+ // the change and get the new correct value.
+ for (int downstream = mCurrentDownstreams.nextSetBit(0); downstream >= 0;
+ downstream = mCurrentDownstreams.nextSetBit(downstream + 1)) {
+ if (mCurrentEntitlementResults.indexOfKey(downstream) < 0) {
+ if (mNeedReRunProvisioningUi) {
+ mNeedReRunProvisioningUi = false;
+ runUiTetherProvisioning(downstream, config);
+ } else {
+ runSilentTetherProvisioning(downstream, config);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if the device requires a provisioning check in order to enable tethering.
+ *
+ * @param config an object that encapsulates the various tethering configuration elements.
+ * @return a boolean - {@code true} indicating tether provisioning is required by the carrier.
+ */
+ @VisibleForTesting
+ protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
+ if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
+ || config.provisioningApp.length == 0) {
+ return false;
+ }
+ if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+ return false;
+ }
+ return (config.provisioningApp.length == 2);
+ }
+
+ /**
+ * Re-check tethering provisioning for all enabled tether types.
+ * Reference TetheringManager.TETHERING_{@code *} for each tether type.
+ *
+ * @param config an object that encapsulates the various tethering configuration elements.
+ * Note: this method is only called from @{link Tethering.TetherMainSM} on the handler thread.
+ * If there are new callers from different threads, the logic should move to
+ * @{link Tethering.TetherMainSM} handler to avoid race conditions.
+ */
+ public void reevaluateSimCardProvisioning(final TetheringConfiguration config) {
+ if (DBG) mLog.i("reevaluateSimCardProvisioning");
+
+ if (!mHandler.getLooper().isCurrentThread()) {
+ // Except for test, this log should not appear in normal flow.
+ mLog.log("reevaluateSimCardProvisioning() don't run in TetherMainSM thread");
+ }
+ mEntitlementCacheValue.clear();
+ mCurrentEntitlementResults.clear();
+
+ // TODO: refine provisioning check to isTetherProvisioningRequired() ??
+ if (!config.hasMobileHotspotProvisionApp()
+ || carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+ evaluateCellularPermission(config);
+ return;
+ }
+
+ if (mUsingCellularAsUpstream) {
+ maybeRunProvisioning(config);
+ }
+ }
+
+ /**
+ * Get carrier configuration bundle.
+ * @param config an object that encapsulates the various tethering configuration elements.
+ * */
+ public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
+ final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+ .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ if (configManager == null) return null;
+
+ final PersistableBundle carrierConfig = configManager.getConfigForSubId(
+ config.activeDataSubId);
+
+ if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
+ return carrierConfig;
+ }
+
+ return null;
+ }
+
+ // The logic here is aimed solely at confirming that a CarrierConfig exists
+ // and affirms that entitlement checks are not required.
+ //
+ // TODO: find a better way to express this, or alter the checking process
+ // entirely so that this is more intuitive.
+ private boolean carrierConfigAffirmsEntitlementCheckNotRequired(
+ final TetheringConfiguration config) {
+ // Check carrier config for entitlement checks
+ final PersistableBundle carrierConfig = getCarrierConfig(config);
+ if (carrierConfig == null) return false;
+
+ // A CarrierConfigManager was found and it has a config.
+ final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
+ CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
+ return !isEntitlementCheckRequired;
+ }
+
+ /**
+ * Run no UI tethering provisioning check.
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ * @param subId default data subscription ID.
+ */
+ @VisibleForTesting
+ protected Intent runSilentTetherProvisioning(int type, final TetheringConfiguration config) {
+ if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
+ // For silent provisioning, settings would stop tethering when entitlement fail.
+ ResultReceiver receiver = buildProxyReceiver(type, false/* notifyFail */, null);
+
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+ intent.putExtra(EXTRA_RUN_PROVISION, true);
+ intent.putExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION, config.provisioningAppNoUi);
+ intent.putExtra(EXTRA_TETHER_PROVISIONING_RESPONSE, config.provisioningResponse);
+ intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+ intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
+ intent.setComponent(mSilentProvisioningService);
+ // Only admin user can change tethering and SilentTetherProvisioning don't need to
+ // show UI, it is fine to always start setting's background service as system user.
+ mContext.startService(intent);
+ return intent;
+ }
+
+ private void runUiTetherProvisioning(int type, final TetheringConfiguration config) {
+ ResultReceiver receiver = buildProxyReceiver(type, true/* notifyFail */, null);
+ runUiTetherProvisioning(type, config, receiver);
+ }
+
+ /**
+ * Run the UI-enabled tethering provisioning check.
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ * @param subId default data subscription ID.
+ * @param receiver to receive entitlement check result.
+ */
+ @VisibleForTesting
+ protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config,
+ ResultReceiver receiver) {
+ if (DBG) mLog.i("runUiTetherProvisioning: " + type);
+
+ Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI);
+ intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+ intent.putExtra(EXTRA_TETHER_UI_PROVISIONING_APP_NAME, config.provisioningApp);
+ intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+ intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Only launch entitlement UI for system user. Entitlement UI should not appear for other
+ // user because only admin user is allowed to change tethering.
+ mContext.startActivity(intent);
+ return intent;
+ }
+
+ // Not needed to check if this don't run on the handler thread because it's private.
+ private void scheduleProvisioningRechecks(final TetheringConfiguration config) {
+ if (mProvisioningRecheckAlarm == null) {
+ final int period = config.provisioningCheckPeriod;
+ if (period <= 0) return;
+
+ Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
+ mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
+ Context.ALARM_SERVICE);
+ long periodMs = period * MS_PER_HOUR;
+ long firstAlarmTime = SystemClock.elapsedRealtime() + periodMs;
+ alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, firstAlarmTime, periodMs,
+ mProvisioningRecheckAlarm);
+ }
+ }
+
+ private void cancelTetherProvisioningRechecks() {
+ if (mProvisioningRecheckAlarm != null) {
+ AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
+ Context.ALARM_SERVICE);
+ alarmManager.cancel(mProvisioningRecheckAlarm);
+ mProvisioningRecheckAlarm = null;
+ }
+ }
+
+ private void evaluateCellularPermission(final TetheringConfiguration config) {
+ final boolean permitted = isCellularUpstreamPermitted(config);
+
+ if (DBG) {
+ mLog.i("Cellular permission change from " + mLastCellularUpstreamPermitted
+ + " to " + permitted);
+ }
+
+ if (mLastCellularUpstreamPermitted != permitted) {
+ mLog.log("Cellular permission change: " + permitted);
+ mPermissionChangeCallback.run();
+ }
+ // Only schedule periodic re-check when tether is provisioned
+ // and the result is ok.
+ if (permitted && mCurrentEntitlementResults.size() > 0) {
+ scheduleProvisioningRechecks(config);
+ } else {
+ cancelTetherProvisioningRechecks();
+ }
+ mLastCellularUpstreamPermitted = permitted;
+ }
+
+ /**
+ * Add the mapping between provisioning result and tethering type.
+ * Notify UpstreamNetworkMonitor if Cellular permission changes.
+ *
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ * @param resultCode Provisioning result
+ */
+ protected void addDownstreamMapping(int type, int resultCode) {
+ mLog.i("addDownstreamMapping: " + type + ", result: " + resultCode
+ + " ,TetherTypeRequested: " + mCurrentDownstreams.get(type));
+ if (!mCurrentDownstreams.get(type)) return;
+
+ mCurrentEntitlementResults.put(type, resultCode);
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ evaluateCellularPermission(config);
+ }
+
+ /**
+ * Remove the mapping for input tethering type.
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ */
+ protected void removeDownstreamMapping(int type) {
+ mLog.i("removeDownstreamMapping: " + type);
+ mCurrentEntitlementResults.delete(type);
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ evaluateCellularPermission(config);
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_PROVISIONING_ALARM.equals(intent.getAction())) {
+ mLog.log("Received provisioning alarm");
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ reevaluateSimCardProvisioning(config);
+ }
+ }
+ };
+
+ private static boolean isValidDownstreamType(int type) {
+ switch (type) {
+ case TETHERING_BLUETOOTH:
+ case TETHERING_ETHERNET:
+ case TETHERING_USB:
+ case TETHERING_WIFI:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Dump the infromation of EntitlementManager.
+ * @param pw {@link PrintWriter} is used to print formatted
+ */
+ public void dump(PrintWriter pw) {
+ pw.print("isCellularUpstreamPermitted: ");
+ pw.println(isCellularUpstreamPermitted());
+ for (int type = mCurrentDownstreams.nextSetBit(0); type >= 0;
+ type = mCurrentDownstreams.nextSetBit(type + 1)) {
+ pw.print("Type: ");
+ pw.print(typeString(type));
+ if (mCurrentEntitlementResults.indexOfKey(type) > -1) {
+ pw.print(", Value: ");
+ pw.println(errorString(mCurrentEntitlementResults.get(type)));
+ } else {
+ pw.println(", Value: empty");
+ }
+ }
+ pw.print("Exempted: [");
+ for (int type = mExemptedDownstreams.nextSetBit(0); type >= 0;
+ type = mExemptedDownstreams.nextSetBit(type + 1)) {
+ pw.print(typeString(type));
+ pw.print(", ");
+ }
+ pw.println("]");
+ }
+
+ private static String typeString(int type) {
+ switch (type) {
+ case TETHERING_BLUETOOTH: return "TETHERING_BLUETOOTH";
+ case TETHERING_INVALID: return "TETHERING_INVALID";
+ case TETHERING_USB: return "TETHERING_USB";
+ case TETHERING_WIFI: return "TETHERING_WIFI";
+ default:
+ return String.format("TETHERING UNKNOWN TYPE (%d)", type);
+ }
+ }
+
+ private static String errorString(int value) {
+ switch (value) {
+ case TETHER_ERROR_ENTITLEMENT_UNKNOWN: return "TETHER_ERROR_ENTITLEMENT_UNKONWN";
+ case TETHER_ERROR_NO_ERROR: return "TETHER_ERROR_NO_ERROR";
+ case TETHER_ERROR_PROVISIONING_FAILED: return "TETHER_ERROR_PROVISIONING_FAILED";
+ default:
+ return String.format("UNKNOWN ERROR (%d)", value);
+ }
+ }
+
+ private ResultReceiver buildProxyReceiver(int type, boolean notifyFail,
+ final ResultReceiver receiver) {
+ ResultReceiver rr = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ int updatedCacheValue = updateEntitlementCacheValue(type, resultCode);
+ addDownstreamMapping(type, updatedCacheValue);
+ if (updatedCacheValue == TETHER_ERROR_PROVISIONING_FAILED && notifyFail) {
+ mListener.onUiEntitlementFailed(type);
+ }
+ if (receiver != null) receiver.send(updatedCacheValue, null);
+ }
+ };
+
+ return writeToParcel(rr);
+ }
+
+ // Instances of ResultReceiver need to be public classes for remote processes to be able
+ // to load them (otherwise, ClassNotFoundException). For private classes, this method
+ // performs a trick : round-trip parceling any instance of ResultReceiver will return a
+ // vanilla instance of ResultReceiver sharing the binder token with the original receiver.
+ // The binder token has a reference to the original instance of the private class and will
+ // still call its methods, and can be sent over. However it cannot be used for anything
+ // else than sending over a Binder call.
+ // While round-trip parceling is not great, there is currently no other way of generating
+ // a vanilla instance of ResultReceiver because all its fields are private.
+ private ResultReceiver writeToParcel(final ResultReceiver receiver) {
+ Parcel parcel = Parcel.obtain();
+ receiver.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return receiverForSending;
+ }
+
+ /**
+ * Update the last entitlement value to internal cache
+ *
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ * @param resultCode last entitlement value
+ * @return the last updated entitlement value
+ */
+ private int updateEntitlementCacheValue(int type, int resultCode) {
+ if (DBG) {
+ mLog.i("updateEntitlementCacheValue: " + type + ", result: " + resultCode);
+ }
+ if (resultCode == TETHER_ERROR_NO_ERROR) {
+ mEntitlementCacheValue.put(type, resultCode);
+ return resultCode;
+ } else {
+ mEntitlementCacheValue.put(type, TETHER_ERROR_PROVISIONING_FAILED);
+ return TETHER_ERROR_PROVISIONING_FAILED;
+ }
+ }
+
+ /** Get the last value of the tethering entitlement check. */
+ public void requestLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
+ boolean showEntitlementUi) {
+ if (!isValidDownstreamType(downstream)) {
+ receiver.send(TETHER_ERROR_ENTITLEMENT_UNKNOWN, null);
+ return;
+ }
+
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ if (!isTetherProvisioningRequired(config)) {
+ receiver.send(TETHER_ERROR_NO_ERROR, null);
+ return;
+ }
+
+ final int cacheValue = mEntitlementCacheValue.get(
+ downstream, TETHER_ERROR_ENTITLEMENT_UNKNOWN);
+ if (cacheValue == TETHER_ERROR_NO_ERROR || !showEntitlementUi) {
+ receiver.send(cacheValue, null);
+ } else {
+ ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver);
+ runUiTetherProvisioning(downstream, config, proxy);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
new file mode 100644
index 0000000..f3dcaa2
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
@@ -0,0 +1,328 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.RouteInfo;
+import android.net.ip.IpServer;
+import android.net.util.NetworkConstants;
+import android.net.util.SharedLog;
+import android.util.Log;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Random;
+
+
+/**
+ * IPv6 tethering is rather different from IPv4 owing to the absence of NAT.
+ * This coordinator is responsible for evaluating the dedicated prefixes
+ * assigned to the device and deciding how to divvy them up among downstream
+ * interfaces.
+ *
+ * @hide
+ */
+public class IPv6TetheringCoordinator {
+ private static final String TAG = IPv6TetheringCoordinator.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private static class Downstream {
+ public final IpServer ipServer;
+ public final int mode; // IpServer.STATE_*
+ // Used to append to a ULA /48, constructing a ULA /64 for local use.
+ public final short subnetId;
+
+ Downstream(IpServer ipServer, int mode, short subnetId) {
+ this.ipServer = ipServer;
+ this.mode = mode;
+ this.subnetId = subnetId;
+ }
+ }
+
+ private final ArrayList<IpServer> mNotifyList;
+ private final SharedLog mLog;
+ // NOTE: mActiveDownstreams is a list and not a hash data structure because
+ // we keep active downstreams in arrival order. This is done so /64s can
+ // be parceled out on a "first come, first served" basis and a /64 used by
+ // a downstream that is no longer active can be redistributed to any next
+ // waiting active downstream (again, in arrival order).
+ private final LinkedList<Downstream> mActiveDownstreams;
+ private final byte[] mUniqueLocalPrefix;
+ private short mNextSubnetId;
+ private UpstreamNetworkState mUpstreamNetworkState;
+
+ public IPv6TetheringCoordinator(ArrayList<IpServer> notifyList, SharedLog log) {
+ mNotifyList = notifyList;
+ mLog = log.forSubComponent(TAG);
+ mActiveDownstreams = new LinkedList<>();
+ mUniqueLocalPrefix = generateUniqueLocalPrefix();
+ mNextSubnetId = 0;
+ }
+
+ /** Add active downstream to ipv6 tethering candidate list. */
+ public void addActiveDownstream(IpServer downstream, int mode) {
+ if (findDownstream(downstream) == null) {
+ // Adding a new downstream appends it to the list. Adding a
+ // downstream a second time without first removing it has no effect.
+ // We never change the mode of a downstream except by first removing
+ // it and then re-adding it (with its new mode specified);
+ if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) {
+ // Make sure subnet IDs are always positive. They are appended
+ // to a ULA /48 to make a ULA /64 for local use.
+ mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1);
+ }
+ updateIPv6TetheringInterfaces();
+ }
+ }
+
+ /** Remove downstream from ipv6 tethering candidate list. */
+ public void removeActiveDownstream(IpServer downstream) {
+ stopIPv6TetheringOn(downstream);
+ if (mActiveDownstreams.remove(findDownstream(downstream))) {
+ updateIPv6TetheringInterfaces();
+ }
+
+ // When tethering is stopping we can reset the subnet counter.
+ if (mNotifyList.isEmpty()) {
+ if (!mActiveDownstreams.isEmpty()) {
+ Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty.");
+ }
+ mNextSubnetId = 0;
+ }
+ }
+
+ /**
+ * Call when UpstreamNetworkState may be changed.
+ * If upstream has ipv6 for tethering, update this new UpstreamNetworkState
+ * to IpServer. Otherwise stop ipv6 tethering on downstream interfaces.
+ */
+ public void updateUpstreamNetworkState(UpstreamNetworkState ns) {
+ if (VDBG) {
+ Log.d(TAG, "updateUpstreamNetworkState: " + toDebugString(ns));
+ }
+ if (TetheringInterfaceUtils.getIPv6Interface(ns) == null) {
+ stopIPv6TetheringOnAllInterfaces();
+ setUpstreamNetworkState(null);
+ return;
+ }
+
+ if (mUpstreamNetworkState != null
+ && !ns.network.equals(mUpstreamNetworkState.network)) {
+ stopIPv6TetheringOnAllInterfaces();
+ }
+
+ setUpstreamNetworkState(ns);
+ updateIPv6TetheringInterfaces();
+ }
+
+ private void stopIPv6TetheringOnAllInterfaces() {
+ for (IpServer ipServer : mNotifyList) {
+ stopIPv6TetheringOn(ipServer);
+ }
+ }
+
+ private void setUpstreamNetworkState(UpstreamNetworkState ns) {
+ if (ns == null) {
+ mUpstreamNetworkState = null;
+ } else {
+ // Make a deep copy of the parts we need.
+ mUpstreamNetworkState = new UpstreamNetworkState(
+ new LinkProperties(ns.linkProperties),
+ new NetworkCapabilities(ns.networkCapabilities),
+ new Network(ns.network));
+ }
+
+ mLog.log("setUpstreamNetworkState: " + toDebugString(mUpstreamNetworkState));
+ }
+
+ private void updateIPv6TetheringInterfaces() {
+ for (IpServer ipServer : mNotifyList) {
+ final LinkProperties lp = getInterfaceIPv6LinkProperties(ipServer);
+ ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, getTtlAdjustment(), 0, lp);
+ break;
+ }
+ }
+
+ private int getTtlAdjustment() {
+ if (mUpstreamNetworkState == null || mUpstreamNetworkState.networkCapabilities == null) {
+ return 0;
+ }
+
+ // If upstream is cellular, set the TTL in Router Advertisements to "network-set TTL" - 1
+ // for carrier requirement.
+ if (mUpstreamNetworkState.networkCapabilities.hasTransport(
+ NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ return -1;
+ }
+
+ // For other non-cellular upstream, set TTL as "network-set TTL" + 1 to preventing arbitrary
+ // distinction between tethered and untethered traffic.
+ return 1;
+ }
+
+ private LinkProperties getInterfaceIPv6LinkProperties(IpServer ipServer) {
+ final Downstream ds = findDownstream(ipServer);
+ if (ds == null) return null;
+
+ if (ds.mode == IpServer.STATE_LOCAL_ONLY) {
+ // Build a Unique Locally-assigned Prefix configuration.
+ return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
+ }
+
+ // This downstream is in IpServer.STATE_TETHERED mode.
+ if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) {
+ return null;
+ }
+
+ // NOTE: Here, in future, we would have policies to decide how to divvy
+ // up the available dedicated prefixes among downstream interfaces.
+ // At this time we have no such mechanism--we only support tethering
+ // IPv6 toward the oldest (first requested) active downstream.
+
+ final Downstream currentActive = mActiveDownstreams.peek();
+ if (currentActive != null && currentActive.ipServer == ipServer) {
+ final LinkProperties lp = getIPv6OnlyLinkProperties(
+ mUpstreamNetworkState.linkProperties);
+ if (lp.hasIpv6DefaultRoute() && lp.hasGlobalIpv6Address()) {
+ return lp;
+ }
+ }
+
+ return null;
+ }
+
+ Downstream findDownstream(IpServer ipServer) {
+ for (Downstream ds : mActiveDownstreams) {
+ if (ds.ipServer == ipServer) return ds;
+ }
+ return null;
+ }
+
+ private static LinkProperties getIPv6OnlyLinkProperties(LinkProperties lp) {
+ final LinkProperties v6only = new LinkProperties();
+ if (lp == null) {
+ return v6only;
+ }
+
+ // NOTE: At this time we don't copy over any information about any
+ // stacked links. No current stacked link configuration has IPv6.
+
+ v6only.setInterfaceName(lp.getInterfaceName());
+
+ v6only.setMtu(lp.getMtu());
+
+ for (LinkAddress linkAddr : lp.getLinkAddresses()) {
+ if (linkAddr.isGlobalPreferred() && linkAddr.getPrefixLength() == 64) {
+ v6only.addLinkAddress(linkAddr);
+ }
+ }
+
+ for (RouteInfo routeInfo : lp.getRoutes()) {
+ final IpPrefix destination = routeInfo.getDestination();
+ if ((destination.getAddress() instanceof Inet6Address)
+ && (destination.getPrefixLength() <= 64)) {
+ v6only.addRoute(routeInfo);
+ }
+ }
+
+ for (InetAddress dnsServer : lp.getDnsServers()) {
+ if (isIPv6GlobalAddress(dnsServer)) {
+ // For now we include ULAs.
+ v6only.addDnsServer(dnsServer);
+ }
+ }
+
+ v6only.setDomains(lp.getDomains());
+
+ return v6only;
+ }
+
+ // TODO: Delete this and switch to LinkAddress#isGlobalPreferred once we
+ // announce our own IPv6 address as DNS server.
+ private static boolean isIPv6GlobalAddress(InetAddress ip) {
+ return (ip instanceof Inet6Address)
+ && !ip.isAnyLocalAddress()
+ && !ip.isLoopbackAddress()
+ && !ip.isLinkLocalAddress()
+ && !ip.isSiteLocalAddress()
+ && !ip.isMulticastAddress();
+ }
+
+ private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) {
+ final LinkProperties lp = new LinkProperties();
+
+ final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48);
+ lp.addRoute(new RouteInfo(local48, null, null, RouteInfo.RTN_UNICAST));
+
+ final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64);
+ // Because this is a locally-generated ULA, we don't have an upstream
+ // address. But because the downstream IP address management code gets
+ // its prefix from the upstream's IP address, we create a fake one here.
+ lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64));
+
+ lp.setMtu(NetworkConstants.ETHER_MTU);
+ return lp;
+ }
+
+ private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) {
+ final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length);
+ bytes[7] = (byte) (subnetId >> 8);
+ bytes[8] = (byte) subnetId;
+ final InetAddress addr;
+ try {
+ addr = InetAddress.getByAddress(bytes);
+ } catch (UnknownHostException e) {
+ throw new IllegalStateException("Invalid address length: " + bytes.length, e);
+ }
+ return new IpPrefix(addr, prefixlen);
+ }
+
+ // Generates a Unique Locally-assigned Prefix:
+ //
+ // https://tools.ietf.org/html/rfc4193#section-3.1
+ //
+ // The result is a /48 that can be used for local-only communications.
+ private static byte[] generateUniqueLocalPrefix() {
+ final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte
+ (new Random()).nextBytes(ulp);
+
+ final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN);
+ in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1
+
+ return in6addr;
+ }
+
+ private static String toDebugString(UpstreamNetworkState ns) {
+ if (ns == null) {
+ return "UpstreamNetworkState{null}";
+ }
+ return ns.toString();
+ }
+
+ private static void stopIPv6TetheringOn(IpServer ipServer) {
+ ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
new file mode 100644
index 0000000..beb1821
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
@@ -0,0 +1,884 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
+import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.usage.NetworkStatsManager;
+import android.content.ContentResolver;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkStats;
+import android.net.NetworkStats.Entry;
+import android.net.RouteInfo;
+import android.net.netlink.ConntrackMessage;
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A class to encapsulate the business logic of programming the tethering
+ * hardware offload interface.
+ *
+ * @hide
+ */
+public class OffloadController {
+ private static final String TAG = OffloadController.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final String ANYIP = "0.0.0.0";
+ private static final ForwardedStats EMPTY_STATS = new ForwardedStats();
+
+ @VisibleForTesting
+ enum StatsType {
+ STATS_PER_IFACE,
+ STATS_PER_UID,
+ }
+
+ private enum UpdateType { IF_NEEDED, FORCE };
+
+ private final Handler mHandler;
+ private final OffloadHardwareInterface mHwInterface;
+ private final ContentResolver mContentResolver;
+ @Nullable
+ private final OffloadTetheringStatsProvider mStatsProvider;
+ private final SharedLog mLog;
+ private final HashMap<String, LinkProperties> mDownstreams;
+ private boolean mConfigInitialized;
+ @OffloadHardwareInterface.OffloadHalVersion
+ private int mControlHalVersion;
+ private LinkProperties mUpstreamLinkProperties;
+ // The complete set of offload-exempt prefixes passed in via Tethering from
+ // all upstream and downstream sources.
+ private Set<IpPrefix> mExemptPrefixes;
+ // A strictly "smaller" set of prefixes, wherein offload-approved prefixes
+ // (e.g. downstream on-link prefixes) have been removed and replaced with
+ // prefixes representing only the locally-assigned IP addresses.
+ private Set<String> mLastLocalPrefixStrs;
+
+ // Maps upstream interface names to offloaded traffic statistics.
+ // Always contains the latest value received from the hardware for each interface, regardless of
+ // whether offload is currently running on that interface.
+ private ConcurrentHashMap<String, ForwardedStats> mForwardedStats =
+ new ConcurrentHashMap<>(16, 0.75F, 1);
+
+ private static class InterfaceQuota {
+ public final long warningBytes;
+ public final long limitBytes;
+
+ public static InterfaceQuota MAX_VALUE = new InterfaceQuota(Long.MAX_VALUE, Long.MAX_VALUE);
+
+ InterfaceQuota(long warningBytes, long limitBytes) {
+ this.warningBytes = warningBytes;
+ this.limitBytes = limitBytes;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof InterfaceQuota)) return false;
+ InterfaceQuota that = (InterfaceQuota) o;
+ return warningBytes == that.warningBytes
+ && limitBytes == that.limitBytes;
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (warningBytes * 3 + limitBytes * 5);
+ }
+
+ @Override
+ public String toString() {
+ return "InterfaceQuota{" + "warning=" + warningBytes + ", limit=" + limitBytes + '}';
+ }
+ }
+
+ // Maps upstream interface names to interface quotas.
+ // Always contains the latest value received from the framework for each interface, regardless
+ // of whether offload is currently running (or is even supported) on that interface. Only
+ // includes upstream interfaces that have a quota set.
+ private HashMap<String, InterfaceQuota> mInterfaceQuotas = new HashMap<>();
+
+ // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert
+ // quota is interface independent and global for tether offload. Note that this is only
+ // accessed on the handler thread and in the constructor.
+ private long mRemainingAlertQuota = QUOTA_UNLIMITED;
+ // Runnable that used to schedule the next stats poll.
+ private final Runnable mScheduledPollingTask = () -> {
+ updateStatsForCurrentUpstream();
+ maybeSchedulePollingStats();
+ };
+
+ private int mNatUpdateCallbacksReceived;
+ private int mNatUpdateNetlinkErrors;
+
+ @NonNull
+ private final Dependencies mDeps;
+
+ // TODO: Put more parameters in constructor into dependency object.
+ interface Dependencies {
+ @NonNull
+ TetheringConfiguration getTetherConfig();
+ }
+
+ public OffloadController(Handler h, OffloadHardwareInterface hwi,
+ ContentResolver contentResolver, NetworkStatsManager nsm, SharedLog log,
+ @NonNull Dependencies deps) {
+ mHandler = h;
+ mHwInterface = hwi;
+ mContentResolver = contentResolver;
+ mLog = log.forSubComponent(TAG);
+ mDownstreams = new HashMap<>();
+ mExemptPrefixes = new HashSet<>();
+ mLastLocalPrefixStrs = new HashSet<>();
+ OffloadTetheringStatsProvider provider = new OffloadTetheringStatsProvider();
+ try {
+ nsm.registerNetworkStatsProvider(getClass().getSimpleName(), provider);
+ } catch (RuntimeException e) {
+ Log.wtf(TAG, "Cannot register offload stats provider: " + e);
+ provider = null;
+ }
+ mStatsProvider = provider;
+ mDeps = deps;
+ }
+
+ /** Start hardware offload. */
+ public boolean start() {
+ if (started()) return true;
+
+ if (isOffloadDisabled()) {
+ mLog.i("tethering offload disabled");
+ return false;
+ }
+
+ if (!mConfigInitialized) {
+ mConfigInitialized = mHwInterface.initOffloadConfig();
+ if (!mConfigInitialized) {
+ mLog.i("tethering offload config not supported");
+ stop();
+ return false;
+ }
+ }
+
+ mControlHalVersion = mHwInterface.initOffloadControl(
+ // OffloadHardwareInterface guarantees that these callback
+ // methods are called on the handler passed to it, which is the
+ // same as mHandler, as coordinated by the setup in Tethering.
+ new OffloadHardwareInterface.ControlCallback() {
+ @Override
+ public void onStarted() {
+ if (!started()) return;
+ mLog.log("onStarted");
+ }
+
+ @Override
+ public void onStoppedError() {
+ if (!started()) return;
+ mLog.log("onStoppedError");
+ }
+
+ @Override
+ public void onStoppedUnsupported() {
+ if (!started()) return;
+ mLog.log("onStoppedUnsupported");
+ // Poll for statistics and trigger a sweep of tethering
+ // stats by observers. This might not succeed, but it's
+ // worth trying anyway. We need to do this because from
+ // this point on we continue with software forwarding,
+ // and we need to synchronize stats and limits between
+ // software and hardware forwarding.
+ updateStatsForAllUpstreams();
+ if (mStatsProvider != null) mStatsProvider.pushTetherStats();
+ }
+
+ @Override
+ public void onSupportAvailable() {
+ if (!started()) return;
+ mLog.log("onSupportAvailable");
+
+ // [1] Poll for statistics and trigger a sweep of stats
+ // by observers. We need to do this to ensure that any
+ // limits set take into account any software tethering
+ // traffic that has been happening in the meantime.
+ updateStatsForAllUpstreams();
+ if (mStatsProvider != null) mStatsProvider.pushTetherStats();
+ // [2] (Re)Push all state.
+ computeAndPushLocalPrefixes(UpdateType.FORCE);
+ pushAllDownstreamState();
+ pushUpstreamParameters(null);
+ }
+
+ @Override
+ public void onStoppedLimitReached() {
+ if (!started()) return;
+ mLog.log("onStoppedLimitReached");
+
+ // We cannot reliably determine on which interface the limit was reached,
+ // because the HAL interface does not specify it. We cannot just use the
+ // current upstream, because that might have changed since the time that
+ // the HAL queued the callback.
+ // TODO: rev the HAL so that it provides an interface name.
+
+ updateStatsForCurrentUpstream();
+ if (mStatsProvider != null) {
+ mStatsProvider.pushTetherStats();
+ // Push stats to service does not cause the service react to it
+ // immediately. Inform the service about limit reached.
+ mStatsProvider.notifyLimitReached();
+ }
+ }
+
+ @Override
+ public void onWarningReached() {
+ if (!started()) return;
+ mLog.log("onWarningReached");
+
+ updateStatsForCurrentUpstream();
+ if (mStatsProvider != null) {
+ mStatsProvider.pushTetherStats();
+ mStatsProvider.notifyWarningReached();
+ }
+ }
+
+ @Override
+ public void onNatTimeoutUpdate(int proto,
+ String srcAddr, int srcPort,
+ String dstAddr, int dstPort) {
+ if (!started()) return;
+ updateNatTimeout(proto, srcAddr, srcPort, dstAddr, dstPort);
+ }
+ });
+
+ final boolean isStarted = started();
+ if (!isStarted) {
+ mLog.i("tethering offload control not supported");
+ stop();
+ } else {
+ mLog.log("tethering offload started, version: "
+ + OffloadHardwareInterface.halVerToString(mControlHalVersion));
+ mNatUpdateCallbacksReceived = 0;
+ mNatUpdateNetlinkErrors = 0;
+ maybeSchedulePollingStats();
+ }
+ return isStarted;
+ }
+
+ /** Stop hardware offload. */
+ public void stop() {
+ // Completely stops tethering offload. After this method is called, it is no longer safe to
+ // call any HAL method, no callbacks from the hardware will be delivered, and any in-flight
+ // callbacks must be ignored. Offload may be started again by calling start().
+ final boolean wasStarted = started();
+ updateStatsForCurrentUpstream();
+ mUpstreamLinkProperties = null;
+ mHwInterface.stopOffloadControl();
+ mControlHalVersion = OFFLOAD_HAL_VERSION_NONE;
+ mConfigInitialized = false;
+ if (mHandler.hasCallbacks(mScheduledPollingTask)) {
+ mHandler.removeCallbacks(mScheduledPollingTask);
+ }
+ if (wasStarted) mLog.log("tethering offload stopped");
+ }
+
+ private boolean started() {
+ return mConfigInitialized && mControlHalVersion != OFFLOAD_HAL_VERSION_NONE;
+ }
+
+ @VisibleForTesting
+ class OffloadTetheringStatsProvider extends NetworkStatsProvider {
+ // These stats must only ever be touched on the handler thread.
+ @NonNull
+ private NetworkStats mIfaceStats = new NetworkStats(0L, 0);
+ @NonNull
+ private NetworkStats mUidStats = new NetworkStats(0L, 0);
+
+ /**
+ * A helper function that collect tether stats from local hashmap. Note that this does not
+ * invoke binder call.
+ */
+ @VisibleForTesting
+ @NonNull
+ NetworkStats getTetherStats(@NonNull StatsType how) {
+ NetworkStats stats = new NetworkStats(0L, 0);
+ final int uid = (how == StatsType.STATS_PER_UID) ? UID_TETHERING : UID_ALL;
+
+ for (final Map.Entry<String, ForwardedStats> kv : mForwardedStats.entrySet()) {
+ final ForwardedStats value = kv.getValue();
+ final Entry entry = new Entry(kv.getKey(), uid, SET_DEFAULT, TAG_NONE, METERED_NO,
+ ROAMING_NO, DEFAULT_NETWORK_NO, value.rxBytes, 0L, value.txBytes, 0L, 0L);
+ stats = stats.addEntry(entry);
+ }
+
+ return stats;
+ }
+
+ @Override
+ public void onSetLimit(String iface, long quotaBytes) {
+ onSetWarningAndLimit(iface, QUOTA_UNLIMITED, quotaBytes);
+ }
+
+ @Override
+ public void onSetWarningAndLimit(@NonNull String iface,
+ long warningBytes, long limitBytes) {
+ // Listen for all iface is necessary since upstream might be changed after limit
+ // is set.
+ mHandler.post(() -> {
+ final InterfaceQuota curIfaceQuota = mInterfaceQuotas.get(iface);
+ final InterfaceQuota newIfaceQuota = new InterfaceQuota(
+ warningBytes == QUOTA_UNLIMITED ? Long.MAX_VALUE : warningBytes,
+ limitBytes == QUOTA_UNLIMITED ? Long.MAX_VALUE : limitBytes);
+
+ // If the quota is set to unlimited, the value set to HAL is Long.MAX_VALUE,
+ // which is ~8.4 x 10^6 TiB, no one can actually reach it. Thus, it is not
+ // useful to set it multiple times.
+ // Otherwise, the quota needs to be updated to tell HAL to re-count from now even
+ // if the quota is the same as the existing one.
+ if (null == curIfaceQuota && InterfaceQuota.MAX_VALUE.equals(newIfaceQuota)) {
+ return;
+ }
+
+ if (InterfaceQuota.MAX_VALUE.equals(newIfaceQuota)) {
+ mInterfaceQuotas.remove(iface);
+ } else {
+ mInterfaceQuotas.put(iface, newIfaceQuota);
+ }
+ maybeUpdateDataWarningAndLimit(iface);
+ });
+ }
+
+ /**
+ * Push stats to service, but does not cause a force polling. Note that this can only be
+ * called on the handler thread.
+ */
+ public void pushTetherStats() {
+ // TODO: remove the accumulated stats and report the diff from HAL directly.
+ final NetworkStats ifaceDiff =
+ getTetherStats(StatsType.STATS_PER_IFACE).subtract(mIfaceStats);
+ final NetworkStats uidDiff =
+ getTetherStats(StatsType.STATS_PER_UID).subtract(mUidStats);
+ try {
+ notifyStatsUpdated(0 /* token */, ifaceDiff, uidDiff);
+ mIfaceStats = mIfaceStats.add(ifaceDiff);
+ mUidStats = mUidStats.add(uidDiff);
+ } catch (RuntimeException e) {
+ mLog.e("Cannot report network stats: ", e);
+ }
+ }
+
+ @Override
+ public void onRequestStatsUpdate(int token) {
+ // Do not attempt to update stats by querying the offload HAL
+ // synchronously from a different thread than the Handler thread. http://b/64771555.
+ mHandler.post(() -> {
+ updateStatsForCurrentUpstream();
+ pushTetherStats();
+ });
+ }
+
+ @Override
+ public void onSetAlert(long quotaBytes) {
+ // Ignore set alert calls from HAL V1.1 since the hardware supports set warning now.
+ // Thus, the software polling mechanism is not needed.
+ if (!useStatsPolling()) {
+ return;
+ }
+ // Post it to handler thread since it access remaining quota bytes.
+ mHandler.post(() -> {
+ updateAlertQuota(quotaBytes);
+ maybeSchedulePollingStats();
+ });
+ }
+ }
+
+ private String currentUpstreamInterface() {
+ return (mUpstreamLinkProperties != null)
+ ? mUpstreamLinkProperties.getInterfaceName() : null;
+ }
+
+ private void maybeUpdateStats(String iface) {
+ if (TextUtils.isEmpty(iface)) {
+ return;
+ }
+
+ // Always called on the handler thread.
+ //
+ // Use get()/put() instead of updating ForwardedStats in place because we can be called
+ // concurrently with getTetherStats. In combination with the guarantees provided by
+ // ConcurrentHashMap, this ensures that getTetherStats always gets the most recent copy of
+ // the stats for each interface, and does not observe partial writes where rxBytes is
+ // updated and txBytes is not.
+ ForwardedStats diff = mHwInterface.getForwardedStats(iface);
+ final long usedAlertQuota = diff.rxBytes + diff.txBytes;
+ ForwardedStats base = mForwardedStats.get(iface);
+ if (base != null) {
+ diff.add(base);
+ }
+
+ // Update remaining alert quota if it is still positive.
+ if (mRemainingAlertQuota > 0 && usedAlertQuota > 0) {
+ // Trim to zero if overshoot.
+ final long newQuota = Math.max(mRemainingAlertQuota - usedAlertQuota, 0);
+ updateAlertQuota(newQuota);
+ }
+
+ mForwardedStats.put(iface, diff);
+ // diff is a new object, just created by getForwardedStats(). Therefore, anyone reading from
+ // mForwardedStats (i.e., any caller of getTetherStats) will see the new stats immediately.
+ }
+
+ /**
+ * Update remaining alert quota, fire the {@link NetworkStatsProvider#notifyAlertReached()}
+ * callback when it reaches zero. This can be invoked either from service setting the alert, or
+ * {@code maybeUpdateStats} when updating stats. Note that this can be only called on
+ * handler thread.
+ *
+ * @param newQuota non-negative value to indicate the new quota, or
+ * {@link NetworkStatsProvider#QUOTA_UNLIMITED} to indicate there is no
+ * quota.
+ */
+ private void updateAlertQuota(long newQuota) {
+ if (newQuota < QUOTA_UNLIMITED) {
+ throw new IllegalArgumentException("invalid quota value " + newQuota);
+ }
+ if (mRemainingAlertQuota == newQuota) return;
+
+ mRemainingAlertQuota = newQuota;
+ if (mRemainingAlertQuota == 0) {
+ mLog.i("notifyAlertReached");
+ if (mStatsProvider != null) mStatsProvider.notifyAlertReached();
+ }
+ }
+
+ /**
+ * Schedule polling if needed, this will be stopped if offload has been
+ * stopped or remaining quota reaches zero or upstream is empty.
+ * Note that this can be only called on handler thread.
+ */
+ private void maybeSchedulePollingStats() {
+ if (!isPollingStatsNeeded()) return;
+
+ if (mHandler.hasCallbacks(mScheduledPollingTask)) {
+ mHandler.removeCallbacks(mScheduledPollingTask);
+ }
+ mHandler.postDelayed(mScheduledPollingTask,
+ mDeps.getTetherConfig().getOffloadPollInterval());
+ }
+
+ private boolean isPollingStatsNeeded() {
+ return started() && mRemainingAlertQuota > 0
+ && useStatsPolling()
+ && !TextUtils.isEmpty(currentUpstreamInterface())
+ && mDeps.getTetherConfig() != null
+ && mDeps.getTetherConfig().getOffloadPollInterval()
+ >= DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+ }
+
+ private boolean useStatsPolling() {
+ return mControlHalVersion == OFFLOAD_HAL_VERSION_1_0;
+ }
+
+ private boolean maybeUpdateDataWarningAndLimit(String iface) {
+ // setDataLimit or setDataWarningAndLimit may only be called while offload is occurring
+ // on this upstream.
+ if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
+ return true;
+ }
+
+ final InterfaceQuota quota = mInterfaceQuotas.getOrDefault(iface, InterfaceQuota.MAX_VALUE);
+ final boolean ret;
+ if (mControlHalVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ ret = mHwInterface.setDataWarningAndLimit(iface, quota.warningBytes, quota.limitBytes);
+ } else {
+ ret = mHwInterface.setDataLimit(iface, quota.limitBytes);
+ }
+ return ret;
+ }
+
+ private void updateStatsForCurrentUpstream() {
+ maybeUpdateStats(currentUpstreamInterface());
+ }
+
+ private void updateStatsForAllUpstreams() {
+ // In practice, there should only ever be a single digit number of
+ // upstream interfaces over the lifetime of an active tethering session.
+ // Roughly speaking, imagine a very ambitious one or two of each of the
+ // following interface types: [ "rmnet_data", "wlan", "eth", "rndis" ].
+ for (Map.Entry<String, ForwardedStats> kv : mForwardedStats.entrySet()) {
+ maybeUpdateStats(kv.getKey());
+ }
+ }
+
+ /** Set current tethering upstream LinkProperties. */
+ public void setUpstreamLinkProperties(LinkProperties lp) {
+ if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return;
+
+ final String prevUpstream = currentUpstreamInterface();
+
+ mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null;
+ // Make sure we record this interface in the ForwardedStats map.
+ final String iface = currentUpstreamInterface();
+ if (!TextUtils.isEmpty(iface)) mForwardedStats.putIfAbsent(iface, EMPTY_STATS);
+
+ maybeSchedulePollingStats();
+
+ // TODO: examine return code and decide what to do if programming
+ // upstream parameters fails (probably just wait for a subsequent
+ // onOffloadEvent() callback to tell us offload is available again and
+ // then reapply all state).
+ computeAndPushLocalPrefixes(UpdateType.IF_NEEDED);
+ pushUpstreamParameters(prevUpstream);
+ }
+
+ /** Set local prefixes. */
+ public void setLocalPrefixes(Set<IpPrefix> localPrefixes) {
+ mExemptPrefixes = localPrefixes;
+
+ if (!started()) return;
+ computeAndPushLocalPrefixes(UpdateType.IF_NEEDED);
+ }
+
+ /** Update current downstream LinkProperties. */
+ public void notifyDownstreamLinkProperties(LinkProperties lp) {
+ final String ifname = lp.getInterfaceName();
+ final LinkProperties oldLp = mDownstreams.put(ifname, new LinkProperties(lp));
+ if (Objects.equals(oldLp, lp)) return;
+
+ if (!started()) return;
+ pushDownstreamState(oldLp, lp);
+ }
+
+ private void pushDownstreamState(LinkProperties oldLp, LinkProperties newLp) {
+ final String ifname = newLp.getInterfaceName();
+ final List<RouteInfo> oldRoutes =
+ (oldLp != null) ? oldLp.getRoutes() : Collections.EMPTY_LIST;
+ final List<RouteInfo> newRoutes = newLp.getRoutes();
+
+ // For each old route, if not in new routes: remove.
+ for (RouteInfo ri : oldRoutes) {
+ if (shouldIgnoreDownstreamRoute(ri)) continue;
+ if (!newRoutes.contains(ri)) {
+ mHwInterface.removeDownstreamPrefix(ifname, ri.getDestination().toString());
+ }
+ }
+
+ // For each new route, if not in old routes: add.
+ for (RouteInfo ri : newRoutes) {
+ if (shouldIgnoreDownstreamRoute(ri)) continue;
+ if (!oldRoutes.contains(ri)) {
+ mHwInterface.addDownstreamPrefix(ifname, ri.getDestination().toString());
+ }
+ }
+ }
+
+ private void pushAllDownstreamState() {
+ for (LinkProperties lp : mDownstreams.values()) {
+ pushDownstreamState(null, lp);
+ }
+ }
+
+ /** Remove downstream interface from offload hardware. */
+ public void removeDownstreamInterface(String ifname) {
+ final LinkProperties lp = mDownstreams.remove(ifname);
+ if (lp == null) return;
+
+ if (!started()) return;
+
+ for (RouteInfo route : lp.getRoutes()) {
+ if (shouldIgnoreDownstreamRoute(route)) continue;
+ mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString());
+ }
+ }
+
+ private boolean isOffloadDisabled() {
+ final int defaultDisposition = mHwInterface.getDefaultTetherOffloadDisabled();
+ return (Settings.Global.getInt(
+ mContentResolver, TETHER_OFFLOAD_DISABLED, defaultDisposition) != 0);
+ }
+
+ private boolean pushUpstreamParameters(String prevUpstream) {
+ final String iface = currentUpstreamInterface();
+
+ if (TextUtils.isEmpty(iface)) {
+ final boolean rval = mHwInterface.setUpstreamParameters("", ANYIP, ANYIP, null);
+ // Update stats after we've told the hardware to stop forwarding so
+ // we don't miss packets.
+ maybeUpdateStats(prevUpstream);
+ return rval;
+ }
+
+ // A stacked interface cannot be an upstream for hardware offload.
+ // Consequently, we examine only the primary interface name, look at
+ // getAddresses() rather than getAllAddresses(), and check getRoutes()
+ // rather than getAllRoutes().
+ final ArrayList<String> v6gateways = new ArrayList<>();
+ String v4addr = null;
+ String v4gateway = null;
+
+ for (InetAddress ip : mUpstreamLinkProperties.getAddresses()) {
+ if (ip instanceof Inet4Address) {
+ v4addr = ip.getHostAddress();
+ break;
+ }
+ }
+
+ // Find the gateway addresses of all default routes of either address family.
+ for (RouteInfo ri : mUpstreamLinkProperties.getRoutes()) {
+ if (!ri.hasGateway()) continue;
+
+ final String gateway = ri.getGateway().getHostAddress();
+ final InetAddress address = ri.getDestination().getAddress();
+ if (ri.isDefaultRoute() && address instanceof Inet4Address) {
+ v4gateway = gateway;
+ } else if (ri.isDefaultRoute() && address instanceof Inet6Address) {
+ v6gateways.add(gateway);
+ }
+ }
+
+ boolean success = mHwInterface.setUpstreamParameters(
+ iface, v4addr, v4gateway, (v6gateways.isEmpty() ? null : v6gateways));
+
+ if (!success) {
+ return success;
+ }
+
+ // Update stats after we've told the hardware to change routing so we don't miss packets.
+ maybeUpdateStats(prevUpstream);
+
+ // Data limits can only be set once offload is running on the upstream.
+ success = maybeUpdateDataWarningAndLimit(iface);
+ if (!success) {
+ // If we failed to set a data limit, don't use this upstream, because we don't want to
+ // blow through the data limit that we were told to apply.
+ mLog.log("Setting data limit for " + iface + " failed, disabling offload.");
+ stop();
+ }
+
+ return success;
+ }
+
+ private boolean computeAndPushLocalPrefixes(UpdateType how) {
+ final boolean force = (how == UpdateType.FORCE);
+ final Set<String> localPrefixStrs = computeLocalPrefixStrings(
+ mExemptPrefixes, mUpstreamLinkProperties);
+ if (!force && mLastLocalPrefixStrs.equals(localPrefixStrs)) return true;
+
+ mLastLocalPrefixStrs = localPrefixStrs;
+ return mHwInterface.setLocalPrefixes(new ArrayList<>(localPrefixStrs));
+ }
+
+ // TODO: Factor in downstream LinkProperties once that information is available.
+ private static Set<String> computeLocalPrefixStrings(
+ Set<IpPrefix> localPrefixes, LinkProperties upstreamLinkProperties) {
+ // Create an editable copy.
+ final Set<IpPrefix> prefixSet = new HashSet<>(localPrefixes);
+
+ // TODO: If a downstream interface (not currently passed in) is reusing
+ // the /64 of the upstream (64share) then:
+ //
+ // [a] remove that /64 from the local prefixes
+ // [b] add in /128s for IP addresses on the downstream interface
+ // [c] add in /128s for IP addresses on the upstream interface
+ //
+ // Until downstream information is available here, simply add /128s from
+ // the upstream network; they'll just be redundant with their /64.
+ if (upstreamLinkProperties != null) {
+ for (LinkAddress linkAddr : upstreamLinkProperties.getLinkAddresses()) {
+ if (!linkAddr.isGlobalPreferred()) continue;
+ final InetAddress ip = linkAddr.getAddress();
+ if (!(ip instanceof Inet6Address)) continue;
+ prefixSet.add(new IpPrefix(ip, 128));
+ }
+ }
+
+ final HashSet<String> localPrefixStrs = new HashSet<>();
+ for (IpPrefix pfx : prefixSet) localPrefixStrs.add(pfx.toString());
+ return localPrefixStrs;
+ }
+
+ private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) {
+ // Ignore any link-local routes.
+ final IpPrefix destination = route.getDestination();
+ final LinkAddress linkAddr = new LinkAddress(destination.getAddress(),
+ destination.getPrefixLength());
+ if (!linkAddr.isGlobalPreferred()) return true;
+
+ return false;
+ }
+
+ /** Dump information. */
+ public void dump(IndentingPrintWriter pw) {
+ if (isOffloadDisabled()) {
+ pw.println("Offload disabled");
+ return;
+ }
+ final boolean isStarted = started();
+ pw.println("Offload HALs " + (isStarted ? "started" : "not started"));
+ pw.println("Offload Control HAL version: "
+ + OffloadHardwareInterface.halVerToString(mControlHalVersion));
+ LinkProperties lp = mUpstreamLinkProperties;
+ String upstream = (lp != null) ? lp.getInterfaceName() : null;
+ pw.println("Current upstream: " + upstream);
+ pw.println("Exempt prefixes: " + mLastLocalPrefixStrs);
+ pw.println("NAT timeout update callbacks received during the "
+ + (isStarted ? "current" : "last")
+ + " offload session: "
+ + mNatUpdateCallbacksReceived);
+ pw.println("NAT timeout update netlink errors during the "
+ + (isStarted ? "current" : "last")
+ + " offload session: "
+ + mNatUpdateNetlinkErrors);
+ }
+
+ private void updateNatTimeout(
+ int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) {
+ final String protoName = protoNameFor(proto);
+ if (protoName == null) {
+ mLog.e("Unknown NAT update callback protocol: " + proto);
+ return;
+ }
+
+ final Inet4Address src = parseIPv4Address(srcAddr);
+ if (src == null) {
+ mLog.e("Failed to parse IPv4 address: " + srcAddr);
+ return;
+ }
+
+ if (!isValidUdpOrTcpPort(srcPort)) {
+ mLog.e("Invalid src port: " + srcPort);
+ return;
+ }
+
+ final Inet4Address dst = parseIPv4Address(dstAddr);
+ if (dst == null) {
+ mLog.e("Failed to parse IPv4 address: " + dstAddr);
+ return;
+ }
+
+ if (!isValidUdpOrTcpPort(dstPort)) {
+ mLog.e("Invalid dst port: " + dstPort);
+ return;
+ }
+
+ mNatUpdateCallbacksReceived++;
+ final String natDescription = String.format("%s (%s, %s) -> (%s, %s)",
+ protoName, srcAddr, srcPort, dstAddr, dstPort);
+ if (DBG) {
+ mLog.log("NAT timeout update: " + natDescription);
+ }
+
+ final int timeoutSec = connectionTimeoutUpdateSecondsFor(proto);
+ final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+ proto, src, srcPort, dst, dstPort, timeoutSec);
+
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg);
+ } catch (ErrnoException e) {
+ mNatUpdateNetlinkErrors++;
+ mLog.e("Error updating NAT conntrack entry >" + natDescription + "<: " + e
+ + ", msg: " + NetlinkConstants.hexify(msg));
+ mLog.log("NAT timeout update callbacks received: " + mNatUpdateCallbacksReceived);
+ mLog.log("NAT timeout update netlink errors: " + mNatUpdateNetlinkErrors);
+ }
+ }
+
+ private static Inet4Address parseIPv4Address(String addrString) {
+ try {
+ final InetAddress ip = InetAddresses.parseNumericAddress(addrString);
+ // TODO: Consider other sanitization steps here, including perhaps:
+ // not eql to 0.0.0.0
+ // not within 169.254.0.0/16
+ // not within ::ffff:0.0.0.0/96
+ // not within ::/96
+ // et cetera.
+ if (ip instanceof Inet4Address) {
+ return (Inet4Address) ip;
+ }
+ } catch (IllegalArgumentException iae) { }
+ return null;
+ }
+
+ private static String protoNameFor(int proto) {
+ // OsConstants values are not constant expressions; no switch statement.
+ if (proto == OsConstants.IPPROTO_UDP) {
+ return "UDP";
+ } else if (proto == OsConstants.IPPROTO_TCP) {
+ return "TCP";
+ }
+ return null;
+ }
+
+ private static int connectionTimeoutUpdateSecondsFor(int proto) {
+ // TODO: Replace this with more thoughtful work, perhaps reading from
+ // and maybe writing to any required
+ //
+ // /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_*
+ // /proc/sys/net/netfilter/nf_conntrack_udp_timeout{,_stream}
+ //
+ // entries. TBD.
+ if (proto == OsConstants.IPPROTO_TCP) {
+ // Cf. /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
+ return 432000;
+ } else {
+ // Cf. /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream
+ return 180;
+ }
+ }
+
+ private static boolean isValidUdpOrTcpPort(int port) {
+ return port > 0 && port < 65536;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
new file mode 100644
index 0000000..e3ac660
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -0,0 +1,695 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static android.net.util.TetheringUtils.uint16;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.hardware.tetheroffload.config.V1_0.IOffloadConfig;
+import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
+import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
+import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
+import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
+import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
+import android.net.netlink.NetlinkSocket;
+import android.net.netlink.StructNfGenMsg;
+import android.net.netlink.StructNlMsgHdr;
+import android.net.util.SharedLog;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.NativeHandle;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+
+
+/**
+ * Capture tethering dependencies, for injection.
+ *
+ * @hide
+ */
+public class OffloadHardwareInterface {
+ private static final String TAG = OffloadHardwareInterface.class.getSimpleName();
+ private static final String YIELDS = " -> ";
+ // Change this value to control whether tether offload is enabled or
+ // disabled by default in the absence of an explicit Settings value.
+ // See accompanying unittest to distinguish 0 from non-0 values.
+ private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0;
+ private static final String NO_INTERFACE_NAME = "";
+ private static final String NO_IPV4_ADDRESS = "";
+ private static final String NO_IPV4_GATEWAY = "";
+ // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
+ public static final int NF_NETLINK_CONNTRACK_NEW = 1;
+ public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
+ public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+ // Reference libnetfilter_conntrack/linux_nfnetlink_conntrack.h
+ public static final short NFNL_SUBSYS_CTNETLINK = 1;
+ public static final short IPCTNL_MSG_CT_NEW = 0;
+ public static final short IPCTNL_MSG_CT_GET = 1;
+
+ private final long NETLINK_MESSAGE_TIMEOUT_MS = 500;
+
+ private final Handler mHandler;
+ private final SharedLog mLog;
+ private final Dependencies mDeps;
+ private IOffloadControl mOffloadControl;
+
+ // TODO: Use major-minor version control to prevent from defining new constants.
+ static final int OFFLOAD_HAL_VERSION_NONE = 0;
+ static final int OFFLOAD_HAL_VERSION_1_0 = 1;
+ static final int OFFLOAD_HAL_VERSION_1_1 = 2;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "OFFLOAD_HAL_VERSION_", value = {
+ OFFLOAD_HAL_VERSION_NONE,
+ OFFLOAD_HAL_VERSION_1_0,
+ OFFLOAD_HAL_VERSION_1_1
+ })
+ public @interface OffloadHalVersion {}
+ @OffloadHalVersion
+ private int mOffloadControlVersion = OFFLOAD_HAL_VERSION_NONE;
+
+ @NonNull
+ static String halVerToString(int version) {
+ switch(version) {
+ case OFFLOAD_HAL_VERSION_1_0:
+ return "1.0";
+ case OFFLOAD_HAL_VERSION_1_1:
+ return "1.1";
+ case OFFLOAD_HAL_VERSION_NONE:
+ return "None";
+ default:
+ throw new IllegalArgumentException("Unsupported version int " + version);
+ }
+
+ }
+
+ private TetheringOffloadCallback mTetheringOffloadCallback;
+ private ControlCallback mControlCallback;
+
+ /** The callback to notify status of offload management process. */
+ public static class ControlCallback {
+ /** Offload started. */
+ public void onStarted() {}
+ /**
+ * Offload stopped because an error has occurred in lower layer.
+ */
+ public void onStoppedError() {}
+ /**
+ * Offload stopped because the device has moved to a bearer on which hardware offload is
+ * not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will
+ * likely fail and cannot be presumed to be saved inside of the hardware management process.
+ * Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin
+ * offload again.
+ */
+ public void onStoppedUnsupported() {}
+ /** Indicate that offload is able to proivde support for this time. */
+ public void onSupportAvailable() {}
+ /** Offload stopped because of usage limit reached. */
+ public void onStoppedLimitReached() {}
+ /** Indicate that data warning quota is reached. */
+ public void onWarningReached() {}
+
+ /** Indicate to update NAT timeout. */
+ public void onNatTimeoutUpdate(int proto,
+ String srcAddr, int srcPort,
+ String dstAddr, int dstPort) {}
+ }
+
+ /** The object which records Tx/Rx forwarded bytes. */
+ public static class ForwardedStats {
+ public long rxBytes;
+ public long txBytes;
+
+ public ForwardedStats() {
+ rxBytes = 0;
+ txBytes = 0;
+ }
+
+ @VisibleForTesting
+ public ForwardedStats(long rxBytes, long txBytes) {
+ this.rxBytes = rxBytes;
+ this.txBytes = txBytes;
+ }
+
+ /** Add Tx/Rx bytes. */
+ public void add(ForwardedStats other) {
+ rxBytes += other.rxBytes;
+ txBytes += other.txBytes;
+ }
+
+ /** Returns the string representation of this object. */
+ public String toString() {
+ return String.format("rx:%s tx:%s", rxBytes, txBytes);
+ }
+ }
+
+ public OffloadHardwareInterface(Handler h, SharedLog log) {
+ this(h, log, new Dependencies(log));
+ }
+
+ OffloadHardwareInterface(Handler h, SharedLog log, Dependencies deps) {
+ mHandler = h;
+ mLog = log.forSubComponent(TAG);
+ mDeps = deps;
+ }
+
+ /** Capture OffloadHardwareInterface dependencies, for injection. */
+ static class Dependencies {
+ private final SharedLog mLog;
+
+ Dependencies(SharedLog log) {
+ mLog = log;
+ }
+
+ public IOffloadConfig getOffloadConfig() {
+ try {
+ return IOffloadConfig.getService(true /*retry*/);
+ } catch (RemoteException | NoSuchElementException e) {
+ mLog.e("getIOffloadConfig error " + e);
+ return null;
+ }
+ }
+
+ @NonNull
+ public Pair<IOffloadControl, Integer> getOffloadControl() {
+ IOffloadControl hal = null;
+ int version = OFFLOAD_HAL_VERSION_NONE;
+ try {
+ hal = android.hardware.tetheroffload.control
+ .V1_1.IOffloadControl.getService(true /*retry*/);
+ version = OFFLOAD_HAL_VERSION_1_1;
+ } catch (NoSuchElementException e) {
+ // Unsupported by device.
+ } catch (RemoteException e) {
+ mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_1);
+ }
+ if (hal == null) {
+ try {
+ hal = IOffloadControl.getService(true /*retry*/);
+ version = OFFLOAD_HAL_VERSION_1_0;
+ } catch (NoSuchElementException e) {
+ // Unsupported by device.
+ } catch (RemoteException e) {
+ mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_0);
+ }
+ }
+ return new Pair<IOffloadControl, Integer>(hal, version);
+ }
+
+ public NativeHandle createConntrackSocket(final int groups) {
+ final FileDescriptor fd;
+ try {
+ fd = NetlinkSocket.forProto(OsConstants.NETLINK_NETFILTER);
+ } catch (ErrnoException e) {
+ mLog.e("Unable to create conntrack socket " + e);
+ return null;
+ }
+
+ final SocketAddress sockAddr = SocketUtils.makeNetlinkSocketAddress(0, groups);
+ try {
+ Os.bind(fd, sockAddr);
+ } catch (ErrnoException | SocketException e) {
+ mLog.e("Unable to bind conntrack socket for groups " + groups + " error: " + e);
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ie) {
+ // Nothing we can do here
+ }
+ return null;
+ }
+ try {
+ Os.connect(fd, sockAddr);
+ } catch (ErrnoException | SocketException e) {
+ mLog.e("connect to kernel fail for groups " + groups + " error: " + e);
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ie) {
+ // Nothing we can do here
+ }
+ return null;
+ }
+
+ return new NativeHandle(fd, true);
+ }
+ }
+
+ /** Get default value indicating whether offload is supported. */
+ public int getDefaultTetherOffloadDisabled() {
+ return DEFAULT_TETHER_OFFLOAD_DISABLED;
+ }
+
+ /**
+ * Offload management process need to know conntrack rules to support NAT, but it may not have
+ * permission to create netlink netfilter sockets. Create two netlink netfilter sockets and
+ * share them with offload management process.
+ */
+ public boolean initOffloadConfig() {
+ final IOffloadConfig offloadConfig = mDeps.getOffloadConfig();
+ if (offloadConfig == null) {
+ mLog.e("Could not find IOffloadConfig service");
+ return false;
+ }
+ // Per the IConfigOffload definition:
+ //
+ // h1 provides a file descriptor bound to the following netlink groups
+ // (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY).
+ //
+ // h2 provides a file descriptor bound to the following netlink groups
+ // (NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY).
+ final NativeHandle h1 = mDeps.createConntrackSocket(
+ NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
+ if (h1 == null) return false;
+
+ sendIpv4NfGenMsg(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
+ (short) (NLM_F_REQUEST | NLM_F_DUMP));
+
+ final NativeHandle h2 = mDeps.createConntrackSocket(
+ NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY);
+ if (h2 == null) {
+ closeFdInNativeHandle(h1);
+ return false;
+ }
+
+ final CbResults results = new CbResults();
+ try {
+ offloadConfig.setHandles(h1, h2,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record("initOffloadConfig, setHandles fail", e);
+ return false;
+ }
+ // Explicitly close FDs.
+ closeFdInNativeHandle(h1);
+ closeFdInNativeHandle(h2);
+
+ record("initOffloadConfig, setHandles results:", results);
+ return results.mSuccess;
+ }
+
+ @VisibleForTesting
+ public void sendIpv4NfGenMsg(@NonNull NativeHandle handle, short type, short flags) {
+ final int length = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+ final byte[] msg = new byte[length];
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(msg);
+ byteBuffer.order(ByteOrder.nativeOrder());
+
+ final StructNlMsgHdr nlh = new StructNlMsgHdr();
+ nlh.nlmsg_len = length;
+ nlh.nlmsg_type = type;
+ nlh.nlmsg_flags = flags;
+ nlh.nlmsg_seq = 0;
+ nlh.pack(byteBuffer);
+
+ // Header needs to be added to buffer since a generic netlink request is being sent.
+ final StructNfGenMsg nfh = new StructNfGenMsg((byte) OsConstants.AF_INET);
+ nfh.pack(byteBuffer);
+
+ try {
+ NetlinkSocket.sendMessage(handle.getFileDescriptor(), msg, 0 /* offset */, length,
+ NETLINK_MESSAGE_TIMEOUT_MS);
+ } catch (ErrnoException | InterruptedIOException e) {
+ mLog.e("Unable to send netfilter message, error: " + e);
+ }
+ }
+
+ private void closeFdInNativeHandle(final NativeHandle h) {
+ try {
+ h.close();
+ } catch (IOException | IllegalStateException e) {
+ // IllegalStateException means fd is already closed, do nothing here.
+ // Also nothing we can do if IOException.
+ }
+ }
+
+ /**
+ * Initialize the tethering offload HAL.
+ *
+ * @return one of {@code OFFLOAD_HAL_VERSION_*} represents the HAL version, or
+ * {@link #OFFLOAD_HAL_VERSION_NONE} if failed.
+ */
+ public int initOffloadControl(ControlCallback controlCb) {
+ mControlCallback = controlCb;
+
+ if (mOffloadControl == null) {
+ final Pair<IOffloadControl, Integer> halAndVersion = mDeps.getOffloadControl();
+ mOffloadControl = halAndVersion.first;
+ mOffloadControlVersion = halAndVersion.second;
+ if (mOffloadControl == null) {
+ mLog.e("tethering IOffloadControl.getService() returned null");
+ return OFFLOAD_HAL_VERSION_NONE;
+ }
+ mLog.i("tethering offload control version "
+ + halVerToString(mOffloadControlVersion) + " is supported.");
+ }
+
+ final String logmsg = String.format("initOffloadControl(%s)",
+ (controlCb == null) ? "null"
+ : "0x" + Integer.toHexString(System.identityHashCode(controlCb)));
+
+ mTetheringOffloadCallback = new TetheringOffloadCallback(
+ mHandler, mControlCallback, mLog, mOffloadControlVersion);
+ final CbResults results = new CbResults();
+ try {
+ mOffloadControl.initOffload(
+ mTetheringOffloadCallback,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return OFFLOAD_HAL_VERSION_NONE;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess ? mOffloadControlVersion : OFFLOAD_HAL_VERSION_NONE;
+ }
+
+ /** Stop IOffloadControl. */
+ public void stopOffloadControl() {
+ if (mOffloadControl != null) {
+ try {
+ mOffloadControl.stopOffload(
+ (boolean success, String errMsg) -> {
+ if (!success) mLog.e("stopOffload failed: " + errMsg);
+ });
+ } catch (RemoteException e) {
+ mLog.e("failed to stopOffload: " + e);
+ }
+ }
+ mOffloadControl = null;
+ mTetheringOffloadCallback = null;
+ mControlCallback = null;
+ mLog.log("stopOffloadControl()");
+ }
+
+ /** Get Tx/Rx usage from last query. */
+ public ForwardedStats getForwardedStats(String upstream) {
+ final String logmsg = String.format("getForwardedStats(%s)", upstream);
+
+ final ForwardedStats stats = new ForwardedStats();
+ try {
+ mOffloadControl.getForwardedStats(
+ upstream,
+ (long rxBytes, long txBytes) -> {
+ stats.rxBytes = (rxBytes > 0) ? rxBytes : 0;
+ stats.txBytes = (txBytes > 0) ? txBytes : 0;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return stats;
+ }
+
+ return stats;
+ }
+
+ /** Set local prefixes to offload management process. */
+ public boolean setLocalPrefixes(ArrayList<String> localPrefixes) {
+ final String logmsg = String.format("setLocalPrefixes([%s])",
+ String.join(",", localPrefixes));
+
+ final CbResults results = new CbResults();
+ try {
+ mOffloadControl.setLocalPrefixes(localPrefixes,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return false;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess;
+ }
+
+ /** Set data limit value to offload management process. */
+ public boolean setDataLimit(String iface, long limit) {
+
+ final String logmsg = String.format("setDataLimit(%s, %d)", iface, limit);
+
+ final CbResults results = new CbResults();
+ try {
+ mOffloadControl.setDataLimit(
+ iface, limit,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return false;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess;
+ }
+
+ /** Set data warning and limit value to offload management process. */
+ public boolean setDataWarningAndLimit(String iface, long warning, long limit) {
+ if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) {
+ throw new IllegalArgumentException(
+ "setDataWarningAndLimit is not supported below HAL V1.1");
+ }
+ final String logmsg =
+ String.format("setDataWarningAndLimit(%s, %d, %d)", iface, warning, limit);
+
+ final CbResults results = new CbResults();
+ try {
+ ((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mOffloadControl)
+ .setDataWarningAndLimit(
+ iface, warning, limit,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return false;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess;
+ }
+
+ /** Set upstream parameters to offload management process. */
+ public boolean setUpstreamParameters(
+ String iface, String v4addr, String v4gateway, ArrayList<String> v6gws) {
+ iface = (iface != null) ? iface : NO_INTERFACE_NAME;
+ v4addr = (v4addr != null) ? v4addr : NO_IPV4_ADDRESS;
+ v4gateway = (v4gateway != null) ? v4gateway : NO_IPV4_GATEWAY;
+ v6gws = (v6gws != null) ? v6gws : new ArrayList<>();
+
+ final String logmsg = String.format("setUpstreamParameters(%s, %s, %s, [%s])",
+ iface, v4addr, v4gateway, String.join(",", v6gws));
+
+ final CbResults results = new CbResults();
+ try {
+ mOffloadControl.setUpstreamParameters(
+ iface, v4addr, v4gateway, v6gws,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return false;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess;
+ }
+
+ /** Add downstream prefix to offload management process. */
+ public boolean addDownstreamPrefix(String ifname, String prefix) {
+ final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix);
+
+ final CbResults results = new CbResults();
+ try {
+ mOffloadControl.addDownstream(ifname, prefix,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return false;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess;
+ }
+
+ /** Remove downstream prefix from offload management process. */
+ public boolean removeDownstreamPrefix(String ifname, String prefix) {
+ final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix);
+
+ final CbResults results = new CbResults();
+ try {
+ mOffloadControl.removeDownstream(ifname, prefix,
+ (boolean success, String errMsg) -> {
+ results.mSuccess = success;
+ results.mErrMsg = errMsg;
+ });
+ } catch (RemoteException e) {
+ record(logmsg, e);
+ return false;
+ }
+
+ record(logmsg, results);
+ return results.mSuccess;
+ }
+
+ private void record(String msg, Throwable t) {
+ mLog.e(msg + YIELDS + "exception: " + t);
+ }
+
+ private void record(String msg, CbResults results) {
+ final String logmsg = msg + YIELDS + results;
+ if (!results.mSuccess) {
+ mLog.e(logmsg);
+ } else {
+ mLog.log(logmsg);
+ }
+ }
+
+ private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub {
+ public final Handler handler;
+ public final ControlCallback controlCb;
+ public final SharedLog log;
+ private final int mOffloadControlVersion;
+
+ TetheringOffloadCallback(
+ Handler h, ControlCallback cb, SharedLog sharedLog, int offloadControlVersion) {
+ handler = h;
+ controlCb = cb;
+ log = sharedLog;
+ this.mOffloadControlVersion = offloadControlVersion;
+ }
+
+ private void handleOnEvent(int event) {
+ switch (event) {
+ case OffloadCallbackEvent.OFFLOAD_STARTED:
+ controlCb.onStarted();
+ break;
+ case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR:
+ controlCb.onStoppedError();
+ break;
+ case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED:
+ controlCb.onStoppedUnsupported();
+ break;
+ case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE:
+ controlCb.onSupportAvailable();
+ break;
+ case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED:
+ controlCb.onStoppedLimitReached();
+ break;
+ case android.hardware.tetheroffload.control
+ .V1_1.OffloadCallbackEvent.OFFLOAD_WARNING_REACHED:
+ controlCb.onWarningReached();
+ break;
+ default:
+ log.e("Unsupported OffloadCallbackEvent: " + event);
+ }
+ }
+
+ @Override
+ public void onEvent(int event) {
+ // The implementation should never call onEvent()) if the event is already reported
+ // through newer callback.
+ if (mOffloadControlVersion > OFFLOAD_HAL_VERSION_1_0) {
+ Log.wtf(TAG, "onEvent(" + event + ") fired on HAL "
+ + halVerToString(mOffloadControlVersion));
+ }
+ handler.post(() -> {
+ handleOnEvent(event);
+ });
+ }
+
+ @Override
+ public void onEvent_1_1(int event) {
+ if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) {
+ Log.wtf(TAG, "onEvent_1_1(" + event + ") fired on HAL "
+ + halVerToString(mOffloadControlVersion));
+ return;
+ }
+ handler.post(() -> {
+ handleOnEvent(event);
+ });
+ }
+
+ @Override
+ public void updateTimeout(NatTimeoutUpdate params) {
+ handler.post(() -> {
+ controlCb.onNatTimeoutUpdate(
+ networkProtocolToOsConstant(params.proto),
+ params.src.addr, uint16(params.src.port),
+ params.dst.addr, uint16(params.dst.port));
+ });
+ }
+ }
+
+ private static int networkProtocolToOsConstant(int proto) {
+ switch (proto) {
+ case NetworkProtocol.TCP: return OsConstants.IPPROTO_TCP;
+ case NetworkProtocol.UDP: return OsConstants.IPPROTO_UDP;
+ default:
+ // The caller checks this value and will log an error. Just make
+ // sure it won't collide with valid OsContants.IPPROTO_* values.
+ return -Math.abs(proto);
+ }
+ }
+
+ private static class CbResults {
+ boolean mSuccess;
+ String mErrMsg;
+
+ @Override
+ public String toString() {
+ if (mSuccess) {
+ return "ok";
+ } else {
+ return "fail: " + mErrMsg;
+ }
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
new file mode 100644
index 0000000..4f616cd
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -0,0 +1,416 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.util.PrefixUtils.asIpPrefix;
+
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;
+
+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.Network;
+import android.net.ip.IpServer;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * This class coordinate IP addresses conflict problem.
+ *
+ * Tethering downstream IP addresses may conflict with network assigned addresses. This
+ * coordinator is responsible for recording all of network assigned addresses and dispatched
+ * free address to downstream interfaces.
+ *
+ * This class is not thread-safe and should be accessed on the same tethering internal thread.
+ * @hide
+ */
+public class PrivateAddressCoordinator {
+ public static final int PREFIX_LENGTH = 24;
+
+ // Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
+ // address may be requested before coordinator get current upstream notification. To ensure
+ // coordinator do not select conflict downstream prefix, mUpstreamPrefixMap would not be cleared
+ // when tethering is down. Instead tethering would remove all deprecated upstreams from
+ // mUpstreamPrefixMap when tethering is starting. See #maybeRemoveDeprecatedUpstreams().
+ private final ArrayMap<Network, List<IpPrefix>> mUpstreamPrefixMap;
+ private final ArraySet<IpServer> mDownstreams;
+ 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;
+ // keyed by downstream type(TetheringManager.TETHERING_*).
+ private final SparseArray<LinkAddress> mCachedAddresses;
+
+ public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
+ mDownstreams = new ArraySet<>();
+ mUpstreamPrefixMap = new ArrayMap<>();
+ mConnectivityMgr = (ConnectivityManager) context.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ mConfig = config;
+ mCachedAddresses = new SparseArray<>();
+ // Reserved static addresses for bluetooth and wifi p2p.
+ mCachedAddresses.put(TETHERING_BLUETOOTH, new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
+ mCachedAddresses.put(TETHERING_WIFI_P2P, new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
+
+ mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16")));
+ if (config.isSelectAllPrefixRangeEnabled()) {
+ mTetheringPrefixes.add(new IpPrefix("172.16.0.0/12"));
+ mTetheringPrefixes.add(new IpPrefix("10.0.0.0/8"));
+ }
+ }
+
+ /**
+ * 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) {
+ // 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);
+ return;
+ }
+
+ final ArrayList<IpPrefix> ipv4Prefixes = getIpv4Prefixes(
+ ns.linkProperties.getAllLinkAddresses());
+ if (ipv4Prefixes.isEmpty()) {
+ removeUpstreamPrefix(ns.network);
+ return;
+ }
+
+ mUpstreamPrefixMap.put(ns.network, ipv4Prefixes);
+ handleMaybePrefixConflict(ipv4Prefixes);
+ }
+
+ private ArrayList<IpPrefix> getIpv4Prefixes(final List<LinkAddress> linkAddresses) {
+ final ArrayList<IpPrefix> list = new ArrayList<>();
+ for (LinkAddress address : linkAddresses) {
+ if (!address.isIpv4()) continue;
+
+ list.add(asIpPrefix(address));
+ }
+
+ return list;
+ }
+
+ private void handleMaybePrefixConflict(final List<IpPrefix> prefixes) {
+ for (IpServer downstream : mDownstreams) {
+ final IpPrefix target = getDownstreamPrefix(downstream);
+
+ for (IpPrefix source : prefixes) {
+ if (isConflictPrefix(source, target)) {
+ downstream.sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
+ break;
+ }
+ }
+ }
+ }
+
+ /** Remove IpPrefix records corresponding to input network. */
+ public void removeUpstreamPrefix(final Network network) {
+ mUpstreamPrefixMap.remove(network);
+ }
+
+ /**
+ * Maybe remove deprecated upstream records, this would be called once tethering started without
+ * any exiting tethered downstream.
+ */
+ public void maybeRemoveDeprecatedUpstreams() {
+ if (mUpstreamPrefixMap.isEmpty()) return;
+
+ // Remove all upstreams that are no longer valid networks
+ final Set<Network> toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet());
+ toBeRemoved.removeAll(asList(mConnectivityMgr.getAllNetworks()));
+
+ mUpstreamPrefixMap.removeAll(toBeRemoved);
+ }
+
+ /**
+ * Pick a random available address and mark its prefix as in use for the provided IpServer,
+ * returns null if there is no available address.
+ */
+ @Nullable
+ public LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) {
+ if (mConfig.shouldEnableWifiP2pDedicatedIp()
+ && ipServer.interfaceType() == TETHERING_WIFI_P2P) {
+ return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
+ }
+
+ final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
+ if (useLastAddress && cachedAddress != null
+ && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
+ mDownstreams.add(ipServer);
+ return cachedAddress;
+ }
+
+ for (IpPrefix prefixRange : mTetheringPrefixes) {
+ final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
+ if (newAddress != null) {
+ mDownstreams.add(ipServer);
+ mCachedAddresses.put(ipServer.interfaceType(), newAddress);
+ return newAddress;
+ }
+ }
+
+ // No available address.
+ return null;
+ }
+
+ private int getPrefixBaseAddress(final IpPrefix prefix) {
+ return inet4AddressToIntHTH((Inet4Address) prefix.getAddress());
+ }
+
+ /**
+ * Check whether input prefix conflict with upstream prefixes or in-use downstream prefixes.
+ * If yes, return one of them.
+ */
+ private IpPrefix getConflictPrefix(final IpPrefix prefix) {
+ final IpPrefix upstream = getConflictWithUpstream(prefix);
+ if (upstream != null) return upstream;
+
+ 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) {
+ // 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);
+
+ // 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 randomPrefixStart = getRandomInt() & ~prefixRangeMask & prefixMask;
+
+ // 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 255, this is a number between 2 and 254.
+ final int subAddress = getSanitizedSubAddr(~prefixMask);
+
+ // 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);
+
+ // 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 IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH);
+
+ final IpPrefix conflictPrefix = getConflictPrefix(prefix);
+
+ if (conflictPrefix == null) return prefix;
+
+ newSubPrefix = getNextSubPrefix(conflictPrefix, prefixRangeMask);
+ }
+
+ return null;
+ }
+
+ /** Get random int which could be used to generate random address. */
+ @VisibleForTesting
+ public int getRandomInt() {
+ return (new Random()).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 subAddrMask) {
+ final int randomSubAddr = getRandomInt() & 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);
+ }
+
+ /** Clear current upstream prefixes records. */
+ public void clearUpstreamPrefixes() {
+ mUpstreamPrefixMap.clear();
+ }
+
+ private IpPrefix getConflictWithUpstream(final IpPrefix prefix) {
+ for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
+ final List<IpPrefix> list = mUpstreamPrefixMap.valueAt(i);
+ for (IpPrefix upstream : list) {
+ if (isConflictPrefix(prefix, upstream)) return upstream;
+ }
+ }
+ return null;
+ }
+
+ private boolean isConflictWithUpstream(final IpPrefix prefix) {
+ return getConflictWithUpstream(prefix) != null;
+ }
+
+ private boolean isConflictPrefix(final IpPrefix prefix1, final IpPrefix prefix2) {
+ if (prefix2.getPrefixLength() < prefix1.getPrefixLength()) {
+ return prefix2.contains(prefix1.getAddress());
+ }
+
+ return prefix1.contains(prefix2.getAddress());
+ }
+
+ // InUse Prefixes are prefixes of mCachedAddresses which are active downstream addresses, last
+ // downstream addresses(reserved for next time) and static addresses(e.g. bluetooth, wifi p2p).
+ private IpPrefix getInUseDownstreamPrefix(final IpPrefix prefix) {
+ for (int i = 0; i < mCachedAddresses.size(); i++) {
+ final IpPrefix downstream = asIpPrefix(mCachedAddresses.valueAt(i));
+ if (isConflictPrefix(prefix, downstream)) return downstream;
+ }
+
+ // IpServer may use manually-defined address (mStaticIpv4ServerAddr) which does not include
+ // in mCachedAddresses.
+ for (IpServer downstream : mDownstreams) {
+ final IpPrefix target = getDownstreamPrefix(downstream);
+
+ if (isConflictPrefix(prefix, target)) return target;
+ }
+
+ return null;
+ }
+
+ @NonNull
+ private IpPrefix getDownstreamPrefix(final IpServer downstream) {
+ final LinkAddress address = downstream.getAddress();
+
+ return asIpPrefix(address);
+ }
+
+ void dump(final IndentingPrintWriter pw) {
+ pw.println("mTetheringPrefixes:");
+ pw.increaseIndent();
+ for (IpPrefix prefix : mTetheringPrefixes) {
+ pw.println(prefix);
+ }
+ pw.decreaseIndent();
+
+ pw.println("mUpstreamPrefixMap:");
+ pw.increaseIndent();
+ for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
+ pw.println(mUpstreamPrefixMap.keyAt(i) + " - " + mUpstreamPrefixMap.valueAt(i));
+ }
+ pw.decreaseIndent();
+
+ pw.println("mDownstreams:");
+ pw.increaseIndent();
+ for (IpServer ipServer : mDownstreams) {
+ pw.println(ipServer.interfaceType() + " - " + ipServer.getAddress());
+ }
+ pw.decreaseIndent();
+
+ pw.println("mCachedAddresses:");
+ pw.increaseIndent();
+ for (int i = 0; i < mCachedAddresses.size(); i++) {
+ pw.println(mCachedAddresses.keyAt(i) + " - " + mCachedAddresses.valueAt(i));
+ }
+ pw.decreaseIndent();
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Key.java b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java
new file mode 100644
index 0000000..a01ea34
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java
@@ -0,0 +1,81 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Key type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif;
+
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress dstMac;
+
+ @Field(order = 2, type = Type.U8, padding = 1)
+ public final short l4proto;
+
+ @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+ public final byte[] src4;
+
+ @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+ public final byte[] dst4;
+
+ @Field(order = 5, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 6, type = Type.UBE16)
+ public final int dstPort;
+
+ public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+ final byte[] src4, final byte[] dst4, final int srcPort,
+ final int dstPort) {
+ Objects.requireNonNull(dstMac);
+
+ this.iif = iif;
+ this.dstMac = dstMac;
+ this.l4proto = l4proto;
+ this.src4 = src4;
+ this.dst4 = dst4;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format(
+ "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+ + "srcPort: %d, dstPort: %d",
+ iif, dstMac, l4proto,
+ Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+ Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ return String.format("Invalid IP address", e);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Value.java b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java
new file mode 100644
index 0000000..03a226c
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java
@@ -0,0 +1,97 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Value type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Value extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long oif;
+
+ // The ethhdr struct which is defined in uapi/linux/if_ether.h
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress ethDstMac;
+ @Field(order = 2, type = Type.EUI48)
+ public final MacAddress ethSrcMac;
+ @Field(order = 3, type = Type.UBE16)
+ public final int ethProto; // Packet type ID field.
+
+ @Field(order = 4, type = Type.U16)
+ public final int pmtu;
+
+ @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+ public final byte[] src46;
+
+ @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+ public final byte[] dst46;
+
+ @Field(order = 7, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 8, type = Type.UBE16)
+ public final int dstPort;
+
+ // TODO: consider using U64.
+ @Field(order = 9, type = Type.U63)
+ public final long lastUsed;
+
+ public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac,
+ @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+ final byte[] src46, final byte[] dst46, final int srcPort,
+ final int dstPort, final long lastUsed) {
+ Objects.requireNonNull(ethDstMac);
+ Objects.requireNonNull(ethSrcMac);
+
+ this.oif = oif;
+ this.ethDstMac = ethDstMac;
+ this.ethSrcMac = ethSrcMac;
+ this.ethProto = ethProto;
+ this.pmtu = pmtu;
+ this.src46 = src46;
+ this.dst46 = dst46;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ this.lastUsed = lastUsed;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format(
+ "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+ + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+ + "lastUsed: %d",
+ oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+ InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+ Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+ lastUsed);
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ return String.format("Invalid IP address", e);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tether6Value.java b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java
new file mode 100644
index 0000000..b3107fd
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java
@@ -0,0 +1,60 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.util.Objects;
+
+/** Value type for downstream and upstream IPv6 forwarding maps. */
+public class Tether6Value extends Struct {
+ @Field(order = 0, type = Type.S32)
+ public final int oif; // The output interface index.
+
+ // The ethhdr struct which is defined in uapi/linux/if_ether.h
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress ethDstMac; // The destination mac address.
+ @Field(order = 2, type = Type.EUI48)
+ public final MacAddress ethSrcMac; // The source mac address.
+ @Field(order = 3, type = Type.UBE16)
+ public final int ethProto; // Packet type ID field.
+
+ @Field(order = 4, type = Type.U16)
+ public final int pmtu; // The maximum L3 output path/route mtu.
+
+ public Tether6Value(final int oif, @NonNull final MacAddress ethDstMac,
+ @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu) {
+ Objects.requireNonNull(ethSrcMac);
+ Objects.requireNonNull(ethDstMac);
+
+ this.oif = oif;
+ this.ethDstMac = ethDstMac;
+ this.ethSrcMac = ethSrcMac;
+ this.ethProto = ethProto;
+ this.pmtu = pmtu;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("oif: %d, dstMac: %s, srcMac: %s, proto: %d, pmtu: %d", oif,
+ ethDstMac, ethSrcMac, ethProto, pmtu);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java b/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java
new file mode 100644
index 0000000..4283c1b
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java
@@ -0,0 +1,31 @@
+/*
+ * 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.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for mapping interface index. */
+public class TetherDevKey extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifIndex; // interface index
+
+ public TetherDevKey(final long ifIndex) {
+ this.ifIndex = ifIndex;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java b/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java
new file mode 100644
index 0000000..1cd99b5
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java
@@ -0,0 +1,31 @@
+/*
+ * 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.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for mapping interface index. */
+public class TetherDevValue extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifIndex; // interface index
+
+ public TetherDevValue(final long ifIndex) {
+ this.ifIndex = ifIndex;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java
new file mode 100644
index 0000000..a08ad4a
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java
@@ -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.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/** The key of BpfMap which is used for bpf offload. */
+public class TetherDownstream6Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif; // The input interface index.
+
+ @Field(order = 1, type = Type.EUI48, padding = 2)
+ public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
+
+ @Field(order = 2, type = Type.ByteArray, arraysize = 16)
+ public final byte[] neigh6; // The destination IPv6 address.
+
+ public TetherDownstream6Key(final long iif, @NonNull final MacAddress dstMac,
+ final byte[] neigh6) {
+ Objects.requireNonNull(dstMac);
+
+ try {
+ final Inet6Address unused = (Inet6Address) InetAddress.getByAddress(neigh6);
+ } catch (ClassCastException | UnknownHostException e) {
+ throw new IllegalArgumentException("Invalid IPv6 address: "
+ + Arrays.toString(neigh6));
+ }
+ this.iif = iif;
+ this.dstMac = dstMac;
+ this.neigh6 = neigh6;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format("iif: %d, dstMac: %s, neigh: %s", iif, dstMac,
+ Inet6Address.getByAddress(neigh6));
+ } catch (UnknownHostException e) {
+ // Should not happen because construtor already verify neigh6.
+ throw new IllegalStateException("Invalid TetherDownstream6Key");
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java
new file mode 100644
index 0000000..bc9bb47
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java
@@ -0,0 +1,53 @@
+/*
+ * 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.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering per-interface limit. */
+public class TetherLimitKey extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifindex; // upstream interface index
+
+ public TetherLimitKey(final long ifindex) {
+ this.ifindex = ifindex;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherLimitKey)) return false;
+
+ final TetherLimitKey that = (TetherLimitKey) obj;
+
+ return ifindex == that.ifindex;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(ifindex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ifindex: %d", ifindex);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java
new file mode 100644
index 0000000..ed7e7d4
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java
@@ -0,0 +1,57 @@
+/*
+ * 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.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The value of BpfMap which is used for tethering per-interface limit. */
+public class TetherLimitValue extends Struct {
+ // Use the signed long variable to store the int64 limit on limit BPF map.
+ // S64 is enough for each interface limit even at 5Gbps for ~468 years.
+ // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+ // Note that QUOTA_UNLIMITED (-1) indicates there is no limit.
+ @Field(order = 0, type = Type.S64)
+ public final long limit;
+
+ public TetherLimitValue(final long limit) {
+ this.limit = limit;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherLimitValue)) return false;
+
+ final TetherLimitValue that = (TetherLimitValue) obj;
+
+ return limit == that.limit;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(limit);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("limit: %d", limit);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
new file mode 100644
index 0000000..5442480
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
@@ -0,0 +1,53 @@
+/*
+ * 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.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsKey extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifindex; // upstream interface index
+
+ public TetherStatsKey(final long ifindex) {
+ this.ifindex = ifindex;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherStatsKey)) return false;
+
+ final TetherStatsKey that = (TetherStatsKey) obj;
+
+ return ifindex == that.ifindex;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(ifindex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ifindex: %d", ifindex);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
new file mode 100644
index 0000000..844d2e8
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
@@ -0,0 +1,80 @@
+/*
+ * 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.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsValue extends Struct {
+ // Use the signed long variable to store the uint64 stats from stats BPF map.
+ // U63 is enough for each data element even at 5Gbps for ~468 years.
+ // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+ @Field(order = 0, type = Type.U63)
+ public final long rxPackets;
+ @Field(order = 1, type = Type.U63)
+ public final long rxBytes;
+ @Field(order = 2, type = Type.U63)
+ public final long rxErrors;
+ @Field(order = 3, type = Type.U63)
+ public final long txPackets;
+ @Field(order = 4, type = Type.U63)
+ public final long txBytes;
+ @Field(order = 5, type = Type.U63)
+ public final long txErrors;
+
+ public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors,
+ final long txPackets, final long txBytes, final long txErrors) {
+ this.rxPackets = rxPackets;
+ this.rxBytes = rxBytes;
+ this.rxErrors = rxErrors;
+ this.txPackets = txPackets;
+ this.txBytes = txBytes;
+ this.txErrors = txErrors;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherStatsValue)) return false;
+
+ final TetherStatsValue that = (TetherStatsValue) obj;
+
+ return rxPackets == that.rxPackets
+ && rxBytes == that.rxBytes
+ && rxErrors == that.rxErrors
+ && txPackets == that.txPackets
+ && txBytes == that.txBytes
+ && txErrors == that.txErrors;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors)
+ ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, "
+ + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets,
+ txBytes, txErrors);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
new file mode 100644
index 0000000..5893885
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
@@ -0,0 +1,41 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.util.Objects;
+
+/** Key type for upstream IPv6 forwarding map. */
+public class TetherUpstream6Key extends Struct {
+ @Field(order = 0, type = Type.S32)
+ public final int iif; // The input interface index.
+
+ @Field(order = 1, type = Type.EUI48, padding = 2)
+ public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
+
+ public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac) {
+ Objects.requireNonNull(dstMac);
+
+ this.iif = iif;
+ this.dstMac = dstMac;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
new file mode 100644
index 0000000..7596380
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -0,0 +1,2494 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.content.pm.PackageManager.GET_ACTIVITIES;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.hardware.usb.UsbManager.USB_CONFIGURED;
+import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
+import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
+import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.TetheringManager.EXTRA_ERRORED_TETHER;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+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_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHERING_WIGIG;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.TetheringManager.toIfaces;
+import static android.net.util.TetheringMessageBase.BASE_MAIN_SM;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_UNSPECIFIED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
+import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
+import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular;
+
+import android.app.usage.NetworkStatsManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager;
+import android.net.EthernetManager;
+import android.net.IIntResultListener;
+import android.net.INetd;
+import android.net.ITetheringEventCallback;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+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.ip.IpServer;
+import android.net.shared.NetdUtils;
+import android.net.util.InterfaceSet;
+import android.net.util.PrefixUtils;
+import android.net.util.SharedLog;
+import android.net.util.TetheringUtils;
+import android.net.util.VersionedBroadcastListener;
+import android.net.wifi.WifiClient;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ *
+ * This class holds much of the business logic to allow Android devices
+ * to act as IP gateways via USB, BT, and WiFi interfaces.
+ */
+public class Tethering {
+
+ private static final String TAG = Tethering.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private static final Class[] sMessageClasses = {
+ Tethering.class, TetherMainSM.class, IpServer.class
+ };
+ private static final SparseArray<String> sMagicDecoderRing =
+ MessageUtils.findMessageNames(sMessageClasses);
+
+ private static final int DUMP_TIMEOUT_MS = 10_000;
+
+ // Keep in sync with NETID_UNSET in system/netd/include/netid_client.h
+ private static final int NETID_UNSET = 0;
+
+ private static class TetherState {
+ public final IpServer ipServer;
+ public int lastState;
+ public int lastError;
+
+ TetherState(IpServer ipServer) {
+ this.ipServer = ipServer;
+ // Assume all state machines start out available and with no errors.
+ lastState = IpServer.STATE_AVAILABLE;
+ lastError = TETHER_ERROR_NO_ERROR;
+ }
+
+ public boolean isCurrentlyServing() {
+ switch (lastState) {
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Cookie added when registering {@link android.net.TetheringManager.TetheringEventCallback}.
+ */
+ private static class CallbackCookie {
+ public final boolean hasListClientsPermission;
+
+ private CallbackCookie(boolean hasListClientsPermission) {
+ this.hasListClientsPermission = hasListClientsPermission;
+ }
+ }
+
+ private final SharedLog mLog = new SharedLog(TAG);
+ private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
+ new RemoteCallbackList<>();
+ // 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 =
+ new SparseArray<>();
+
+ private final Context mContext;
+ private final ArrayMap<String, TetherState> mTetherStates;
+ private final BroadcastReceiver mStateReceiver;
+ private final Looper mLooper;
+ private final TetherMainSM mTetherMainSM;
+ private final OffloadController mOffloadController;
+ private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+ // TODO: Figure out how to merge this and other downstream-tracking objects
+ // into a single coherent structure.
+ private final HashSet<IpServer> mForwardedDownstreams;
+ private final VersionedBroadcastListener mCarrierConfigChange;
+ private final TetheringDependencies mDeps;
+ private final EntitlementManager mEntitlementMgr;
+ private final Handler mHandler;
+ private final INetd mNetd;
+ private final NetdCallback mNetdCallback;
+ private final UserRestrictionActionListener mTetheringRestriction;
+ private final ActiveDataSubIdListener mActiveDataSubIdListener;
+ private final ConnectedClientsTracker mConnectedClientsTracker;
+ private final TetheringThreadExecutor mExecutor;
+ private final TetheringNotificationUpdater mNotificationUpdater;
+ private final UserManager mUserManager;
+ private final BpfCoordinator mBpfCoordinator;
+ private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+ private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
+
+ private volatile TetheringConfiguration mConfig;
+ private InterfaceSet mCurrentUpstreamIfaceSet;
+
+ private boolean mRndisEnabled; // track the RNDIS function enabled state
+ // True iff. WiFi tethering should be started when soft AP is ready.
+ private boolean mWifiTetherRequested;
+ private Network mTetherUpstream;
+ private TetherStatesParcel mTetherStatesParcel;
+ private boolean mDataSaverEnabled = false;
+ private String mWifiP2pTetherInterface = null;
+ private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED;
+
+ private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest;
+ private String mConfiguredEthernetIface;
+ private EthernetCallback mEthernetCallback;
+
+ public Tethering(TetheringDependencies deps) {
+ mLog.mark("Tethering.constructed");
+ mDeps = deps;
+ mContext = mDeps.getContext();
+ mNetd = mDeps.getINetd(mContext);
+ mLooper = mDeps.getTetheringLooper();
+ mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
+
+ mTetherStates = new ArrayMap<>();
+ mConnectedClientsTracker = new ConnectedClientsTracker();
+
+ mTetherMainSM = new TetherMainSM("TetherMain", mLooper, deps);
+ mTetherMainSM.start();
+
+ mHandler = mTetherMainSM.getHandler();
+ mOffloadController = mDeps.getOffloadController(mHandler, mLog,
+ new OffloadController.Dependencies() {
+
+ @Override
+ public TetheringConfiguration getTetherConfig() {
+ return mConfig;
+ }
+ });
+ mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMainSM, mLog,
+ TetherMainSM.EVENT_UPSTREAM_CALLBACK);
+ mForwardedDownstreams = new HashSet<>();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
+ // EntitlementManager will send EVENT_UPSTREAM_PERMISSION_CHANGED when cellular upstream
+ // permission is changed according to entitlement check result.
+ mEntitlementMgr = mDeps.getEntitlementManager(mContext, mHandler, mLog,
+ () -> mTetherMainSM.sendMessage(
+ TetherMainSM.EVENT_UPSTREAM_PERMISSION_CHANGED));
+ mEntitlementMgr.setOnUiEntitlementFailedListener((int downstream) -> {
+ mLog.log("OBSERVED UiEnitlementFailed");
+ stopTethering(downstream);
+ });
+ mEntitlementMgr.setTetheringConfigurationFetcher(() -> {
+ return mConfig;
+ });
+
+ mCarrierConfigChange = new VersionedBroadcastListener(
+ "CarrierConfigChangeListener", mContext, mHandler, filter,
+ (Intent ignored) -> {
+ mLog.log("OBSERVED carrier config change");
+ updateConfiguration();
+ mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
+ });
+
+ mStateReceiver = new StateReceiver();
+
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mTetheringRestriction = new UserRestrictionActionListener(
+ mUserManager, this, mNotificationUpdater);
+ mExecutor = new TetheringThreadExecutor(mHandler);
+ mActiveDataSubIdListener = new ActiveDataSubIdListener(mExecutor);
+ mNetdCallback = new NetdCallback();
+
+ // Load tethering configuration.
+ updateConfiguration();
+ // It is OK for the configuration to be passed to the PrivateAddressCoordinator at
+ // construction time because the only part of the configuration it uses is
+ // shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
+ mPrivateAddressCoordinator = mDeps.getPrivateAddressCoordinator(mContext, mConfig);
+
+ // Must be initialized after tethering configuration is loaded because BpfCoordinator
+ // constructor needs to use the configuration.
+ mBpfCoordinator = mDeps.getBpfCoordinator(
+ new BpfCoordinator.Dependencies() {
+ @NonNull
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ @NonNull
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ @NonNull
+ public NetworkStatsManager getNetworkStatsManager() {
+ return mContext.getSystemService(NetworkStatsManager.class);
+ }
+
+ @NonNull
+ public SharedLog getSharedLog() {
+ return mLog;
+ }
+
+ @Nullable
+ public TetheringConfiguration getTetherConfig() {
+ return mConfig;
+ }
+ });
+
+ startStateMachineUpdaters();
+ }
+
+ /**
+ * Start to register callbacks.
+ * Call this function when tethering is ready to handle callback events.
+ */
+ private void startStateMachineUpdaters() {
+ try {
+ mNetd.registerUnsolicitedEventListener(mNetdCallback);
+ } catch (RemoteException e) {
+ mLog.e("Unable to register netd UnsolicitedEventListener");
+ }
+ mCarrierConfigChange.startListening();
+ mContext.getSystemService(TelephonyManager.class).listen(mActiveDataSubIdListener,
+ PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_STATE);
+ filter.addAction(CONNECTIVITY_ACTION);
+ filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+ filter.addAction(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
+ filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED);
+ mContext.registerReceiver(mStateReceiver, filter, null, mHandler);
+
+ final IntentFilter noUpstreamFilter = new IntentFilter();
+ noUpstreamFilter.addAction(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING);
+ mContext.registerReceiver(
+ mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
+
+ final WifiManager wifiManager = getWifiManager();
+ if (wifiManager != null) {
+ wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+ }
+
+ startTrackDefaultNetwork();
+ }
+
+ private class TetheringThreadExecutor implements Executor {
+ private final Handler mTetherHandler;
+ TetheringThreadExecutor(Handler handler) {
+ mTetherHandler = handler;
+ }
+ @Override
+ public void execute(Runnable command) {
+ if (!mTetherHandler.post(command)) {
+ throw new RejectedExecutionException(mTetherHandler + " is shutting down");
+ }
+ }
+ }
+
+ private class ActiveDataSubIdListener extends PhoneStateListener {
+ ActiveDataSubIdListener(Executor executor) {
+ super(executor);
+ }
+
+ @Override
+ public void onActiveDataSubscriptionIdChanged(int subId) {
+ mLog.log("OBSERVED active data subscription change, from " + mActiveDataSubId
+ + " to " + subId);
+ if (subId == mActiveDataSubId) return;
+
+ mActiveDataSubId = subId;
+ updateConfiguration();
+ mNotificationUpdater.onActiveDataSubscriptionIdChanged(subId);
+ // To avoid launching unexpected provisioning checks, ignore re-provisioning
+ // when no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning()
+ // will be triggered again when CarrierConfig is loaded.
+ if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
+ mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
+ } else {
+ mLog.log("IGNORED reevaluate provisioning, no carrier config loaded");
+ }
+ }
+ }
+
+ private WifiManager getWifiManager() {
+ return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ }
+
+ // NOTE: This is always invoked on the mLooper thread.
+ private void updateConfiguration() {
+ mConfig = mDeps.generateTetheringConfiguration(mContext, mLog, mActiveDataSubId);
+ mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically,
+ mConfig.isDunRequired);
+ reportConfigurationChanged(mConfig.toStableParcelable());
+ }
+
+ private void maybeDunSettingChanged() {
+ final boolean isDunRequired = TetheringConfiguration.checkDunRequired(mContext);
+ if (isDunRequired == mConfig.isDunRequired) return;
+ updateConfiguration();
+ }
+
+ private class NetdCallback extends BaseNetdUnsolicitedEventListener {
+ @Override
+ public void onInterfaceChanged(String ifName, boolean up) {
+ mHandler.post(() -> interfaceStatusChanged(ifName, up));
+ }
+
+ @Override
+ public void onInterfaceLinkStateChanged(String ifName, boolean up) {
+ mHandler.post(() -> interfaceLinkStateChanged(ifName, up));
+ }
+
+ @Override
+ public void onInterfaceAdded(String ifName) {
+ mHandler.post(() -> interfaceAdded(ifName));
+ }
+
+ @Override
+ public void onInterfaceRemoved(String ifName) {
+ mHandler.post(() -> interfaceRemoved(ifName));
+ }
+ }
+
+ private class TetheringSoftApCallback implements WifiManager.SoftApCallback {
+ // TODO: Remove onStateChanged override when this method has default on
+ // WifiManager#SoftApCallback interface.
+ // Wifi listener for state change of the soft AP
+ @Override
+ public void onStateChanged(final int state, final int failureReason) {
+ // Nothing
+ }
+
+ // Called by wifi when the number of soft AP clients changed.
+ @Override
+ public void onConnectedClientsChanged(final List<WifiClient> clients) {
+ updateConnectedClients(clients);
+ }
+ }
+
+ void interfaceStatusChanged(String iface, boolean up) {
+ // Never called directly: only called from interfaceLinkStateChanged.
+ // See NetlinkHandler.cpp: notifyInterfaceChanged.
+ if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up);
+ if (up) {
+ maybeTrackNewInterfaceLocked(iface);
+ } else {
+ if (ifaceNameToType(iface) == TETHERING_BLUETOOTH
+ || ifaceNameToType(iface) == TETHERING_WIGIG) {
+ stopTrackingInterfaceLocked(iface);
+ } else {
+ // Ignore usb0 down after enabling RNDIS.
+ // We will handle disconnect in interfaceRemoved.
+ // Similarly, ignore interface down for WiFi. We monitor WiFi AP status
+ // through the WifiManager.WIFI_AP_STATE_CHANGED_ACTION intent.
+ if (VDBG) Log.d(TAG, "ignore interface down for " + iface);
+ }
+ }
+ }
+
+ void interfaceLinkStateChanged(String iface, boolean up) {
+ interfaceStatusChanged(iface, up);
+ }
+
+ private int ifaceNameToType(String iface) {
+ final TetheringConfiguration cfg = mConfig;
+
+ if (cfg.isWifi(iface)) {
+ return TETHERING_WIFI;
+ } else if (cfg.isWigig(iface)) {
+ return TETHERING_WIGIG;
+ } else if (cfg.isWifiP2p(iface)) {
+ return TETHERING_WIFI_P2P;
+ } else if (cfg.isUsb(iface)) {
+ return TETHERING_USB;
+ } else if (cfg.isBluetooth(iface)) {
+ return TETHERING_BLUETOOTH;
+ } else if (cfg.isNcm(iface)) {
+ return TETHERING_NCM;
+ }
+ return TETHERING_INVALID;
+ }
+
+ void interfaceAdded(String iface) {
+ if (VDBG) Log.d(TAG, "interfaceAdded " + iface);
+ maybeTrackNewInterfaceLocked(iface);
+ }
+
+ void interfaceRemoved(String iface) {
+ if (VDBG) Log.d(TAG, "interfaceRemoved " + iface);
+ stopTrackingInterfaceLocked(iface);
+ }
+
+ void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
+ mHandler.post(() -> {
+ final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
+ request.tetheringType);
+ // 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);
+ }
+ mActiveTetheringRequests.put(request.tetheringType, request);
+
+ if (request.exemptFromEntitlementCheck) {
+ mEntitlementMgr.setExemptedDownstreamType(request.tetheringType);
+ } else {
+ mEntitlementMgr.startProvisioningIfNeeded(request.tetheringType,
+ request.showProvisioningUi);
+ }
+ enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
+ });
+ }
+
+ void stopTethering(int type) {
+ mHandler.post(() -> {
+ mActiveTetheringRequests.remove(type);
+
+ enableTetheringInternal(type, false /* disabled */, null);
+ mEntitlementMgr.stopProvisioningIfNeeded(type);
+ });
+ }
+
+ /**
+ * Enables or disables tethering for the given type. If provisioning is required, it will
+ * schedule provisioning rechecks for the specified interface.
+ */
+ private void enableTetheringInternal(int type, boolean enable,
+ final IIntResultListener listener) {
+ int result = TETHER_ERROR_NO_ERROR;
+ switch (type) {
+ case TETHERING_WIFI:
+ result = setWifiTethering(enable);
+ break;
+ case TETHERING_USB:
+ result = setUsbTethering(enable);
+ break;
+ case TETHERING_BLUETOOTH:
+ setBluetoothTethering(enable, listener);
+ break;
+ case TETHERING_NCM:
+ result = setNcmTethering(enable);
+ break;
+ case TETHERING_ETHERNET:
+ result = setEthernetTethering(enable);
+ break;
+ default:
+ Log.w(TAG, "Invalid tether type.");
+ result = TETHER_ERROR_UNKNOWN_TYPE;
+ }
+
+ // The result of Bluetooth tethering will be sent by #setBluetoothTethering.
+ if (type != TETHERING_BLUETOOTH) {
+ sendTetherResult(listener, result, type);
+ }
+ }
+
+ private void sendTetherResult(final IIntResultListener listener, final int result,
+ final int type) {
+ if (listener != null) {
+ try {
+ listener.onResult(result);
+ } catch (RemoteException e) { }
+ }
+
+ // If changing tethering fail, remove corresponding request
+ // no matter who trigger the start/stop.
+ if (result != TETHER_ERROR_NO_ERROR) mActiveTetheringRequests.remove(type);
+ }
+
+ private int setWifiTethering(final boolean enable) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final WifiManager mgr = getWifiManager();
+ if (mgr == null) {
+ mLog.e("setWifiTethering: failed to get WifiManager!");
+ return TETHER_ERROR_SERVICE_UNAVAIL;
+ }
+ if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
+ || (!enable && mgr.stopSoftAp())) {
+ mWifiTetherRequested = enable;
+ return TETHER_ERROR_NO_ERROR;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+
+ return TETHER_ERROR_INTERNAL_ERROR;
+ }
+
+ private void setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
+ final BluetoothAdapter adapter = mDeps.getBluetoothAdapter();
+ if (adapter == null || !adapter.isEnabled()) {
+ Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
+ + (adapter == null));
+ sendTetherResult(listener, TETHER_ERROR_SERVICE_UNAVAIL, TETHERING_BLUETOOTH);
+ return;
+ }
+
+ adapter.getProfileProxy(mContext, new ServiceListener() {
+ @Override
+ public void onServiceDisconnected(int profile) { }
+
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ // Clear identify is fine because caller already pass tethering permission at
+ // ConnectivityService#startTethering()(or stopTethering) before the control comes
+ // here. Bluetooth will check tethering permission again that there is
+ // Context#getOpPackageName() under BluetoothPan#setBluetoothTethering() to get
+ // caller's package name for permission check.
+ // Calling BluetoothPan#setBluetoothTethering() here means the package name always
+ // be system server. If calling identity is not cleared, that package's uid might
+ // not match calling uid and end up in permission denied.
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ ((BluetoothPan) proxy).setBluetoothTethering(enable);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ // TODO: Enabling bluetooth tethering can fail asynchronously here.
+ // We should figure out a way to bubble up that failure instead of sending success.
+ final int result = (((BluetoothPan) proxy).isTetheringOn() == enable)
+ ? TETHER_ERROR_NO_ERROR
+ : TETHER_ERROR_INTERNAL_ERROR;
+ sendTetherResult(listener, result, TETHERING_BLUETOOTH);
+ adapter.closeProfileProxy(BluetoothProfile.PAN, proxy);
+ }
+ }, BluetoothProfile.PAN);
+ }
+
+ private int setEthernetTethering(final boolean enable) {
+ final EthernetManager em = (EthernetManager) mContext.getSystemService(
+ Context.ETHERNET_SERVICE);
+ if (enable) {
+ if (mEthernetCallback != null) {
+ Log.d(TAG, "Ethernet tethering already started");
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ mEthernetCallback = new EthernetCallback();
+ mEthernetIfaceRequest = em.requestTetheredInterface(mExecutor, mEthernetCallback);
+ } else {
+ stopEthernetTetheringLocked();
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ private void stopEthernetTetheringLocked() {
+ if (mConfiguredEthernetIface != null) {
+ stopTrackingInterfaceLocked(mConfiguredEthernetIface);
+ mConfiguredEthernetIface = null;
+ }
+ if (mEthernetCallback != null) {
+ mEthernetIfaceRequest.release();
+ mEthernetCallback = null;
+ mEthernetIfaceRequest = null;
+ }
+ }
+
+ private class EthernetCallback implements EthernetManager.TetheredInterfaceCallback {
+ @Override
+ public void onAvailable(String iface) {
+ if (this != mEthernetCallback) {
+ // Ethernet callback arrived after Ethernet tethering stopped. Ignore.
+ return;
+ }
+ maybeTrackNewInterfaceLocked(iface, TETHERING_ETHERNET);
+ changeInterfaceState(iface, getRequestedState(TETHERING_ETHERNET));
+ mConfiguredEthernetIface = iface;
+ }
+
+ @Override
+ public void onUnavailable() {
+ if (this != mEthernetCallback) {
+ // onAvailable called after stopping Ethernet tethering.
+ return;
+ }
+ stopEthernetTetheringLocked();
+ }
+ }
+
+ void tether(String iface, int requestedState, final IIntResultListener listener) {
+ mHandler.post(() -> {
+ try {
+ listener.onResult(tether(iface, requestedState));
+ } catch (RemoteException e) { }
+ });
+ }
+
+ private int tether(String iface, int requestedState) {
+ if (DBG) Log.d(TAG, "Tethering " + iface);
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to Tether an unknown iface: " + iface + ", ignoring");
+ return TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ // Ignore the error status of the interface. If the interface is available,
+ // the errors are referring to past tethering attempts anyway.
+ if (tetherState.lastState != IpServer.STATE_AVAILABLE) {
+ Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
+ return TETHER_ERROR_UNAVAIL_IFACE;
+ }
+ // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
+ // processed, this will be a no-op and it will not return an error.
+ //
+ // 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);
+ if (request != null) {
+ mActiveTetheringRequests.delete(type);
+ }
+ tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, requestedState, 0,
+ request);
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ void untether(String iface, final IIntResultListener listener) {
+ mHandler.post(() -> {
+ try {
+ listener.onResult(untether(iface));
+ } catch (RemoteException e) {
+ }
+ });
+ }
+
+ int untether(String iface) {
+ if (DBG) Log.d(TAG, "Untethering " + iface);
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
+ return TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ if (!tetherState.isCurrentlyServing()) {
+ Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
+ return TETHER_ERROR_UNAVAIL_IFACE;
+ }
+ tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_UNREQUESTED);
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ void untetherAll() {
+ stopTethering(TETHERING_WIFI);
+ stopTethering(TETHERING_WIFI_P2P);
+ stopTethering(TETHERING_USB);
+ stopTethering(TETHERING_BLUETOOTH);
+ stopTethering(TETHERING_ETHERNET);
+ }
+
+ @VisibleForTesting
+ int getLastErrorForTest(String iface) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to getLastErrorForTest on an unknown iface :" + iface
+ + ", ignoring");
+ return TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ return tetherState.lastError;
+ }
+
+ private boolean isProvisioningNeededButUnavailable() {
+ return isTetherProvisioningRequired() && !doesEntitlementPackageExist();
+ }
+
+ boolean isTetherProvisioningRequired() {
+ final TetheringConfiguration cfg = mConfig;
+ return mEntitlementMgr.isTetherProvisioningRequired(cfg);
+ }
+
+ private boolean doesEntitlementPackageExist() {
+ // provisioningApp must contain package and class name.
+ if (mConfig.provisioningApp.length != 2) {
+ return false;
+ }
+
+ final PackageManager pm = mContext.getPackageManager();
+ try {
+ pm.getPackageInfo(mConfig.provisioningApp[0], GET_ACTIVITIES);
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ return true;
+ }
+
+ private int getRequestedState(int type) {
+ final TetheringRequestParcel 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
+ : TetheringRequest.getDefaultConnectivityScope(type);
+
+ return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
+ ? IpServer.STATE_LOCAL_ONLY
+ : IpServer.STATE_TETHERED;
+ }
+
+ // TODO: Figure out how to update for local hotspot mode interfaces.
+ private void sendTetherStateChangedBroadcast() {
+ if (!isTetheringSupported()) return;
+
+ final ArrayList<TetheringInterface> available = new ArrayList<>();
+ final ArrayList<TetheringInterface> tethered = new ArrayList<>();
+ final ArrayList<TetheringInterface> localOnly = new ArrayList<>();
+ final ArrayList<TetheringInterface> errored = new ArrayList<>();
+ final ArrayList<Integer> lastErrors = new ArrayList<>();
+
+ final TetheringConfiguration cfg = mConfig;
+
+ int downstreamTypesMask = DOWNSTREAM_NONE;
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final TetherState tetherState = mTetherStates.valueAt(i);
+ final int type = tetherState.ipServer.interfaceType();
+ final String iface = mTetherStates.keyAt(i);
+ final TetheringInterface tetheringIface = new TetheringInterface(type, iface);
+ if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
+ errored.add(tetheringIface);
+ lastErrors.add(tetherState.lastError);
+ } else if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+ available.add(tetheringIface);
+ } else if (tetherState.lastState == IpServer.STATE_LOCAL_ONLY) {
+ localOnly.add(tetheringIface);
+ } else if (tetherState.lastState == IpServer.STATE_TETHERED) {
+ if (cfg.isUsb(iface)) {
+ downstreamTypesMask |= (1 << TETHERING_USB);
+ } else if (cfg.isWifi(iface)) {
+ downstreamTypesMask |= (1 << TETHERING_WIFI);
+ } else if (cfg.isBluetooth(iface)) {
+ downstreamTypesMask |= (1 << TETHERING_BLUETOOTH);
+ }
+ tethered.add(tetheringIface);
+ }
+ }
+
+ mTetherStatesParcel = buildTetherStatesParcel(available, localOnly, tethered, errored,
+ lastErrors);
+ reportTetherStateChanged(mTetherStatesParcel);
+
+ mContext.sendStickyBroadcastAsUser(buildStateChangeIntent(available, localOnly, tethered,
+ errored), UserHandle.ALL);
+ if (DBG) {
+ Log.d(TAG, String.format(
+ "reportTetherStateChanged %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
+ "avail", TextUtils.join(",", available),
+ "local_only", TextUtils.join(",", localOnly),
+ "tether", TextUtils.join(",", tethered),
+ "error", TextUtils.join(",", errored)));
+ }
+
+ mNotificationUpdater.onDownstreamChanged(downstreamTypesMask);
+ }
+
+ private TetherStatesParcel buildTetherStatesParcel(
+ final ArrayList<TetheringInterface> available,
+ final ArrayList<TetheringInterface> localOnly,
+ final ArrayList<TetheringInterface> tethered,
+ final ArrayList<TetheringInterface> errored,
+ final ArrayList<Integer> lastErrors) {
+ final TetherStatesParcel parcel = new TetherStatesParcel();
+
+ parcel.availableList = available.toArray(new TetheringInterface[0]);
+ parcel.tetheredList = tethered.toArray(new TetheringInterface[0]);
+ parcel.localOnlyList = localOnly.toArray(new TetheringInterface[0]);
+ parcel.erroredIfaceList = errored.toArray(new TetheringInterface[0]);
+ parcel.lastErrorList = new int[lastErrors.size()];
+ for (int i = 0; i < lastErrors.size(); i++) {
+ parcel.lastErrorList[i] = lastErrors.get(i);
+ }
+
+ return parcel;
+ }
+
+ private Intent buildStateChangeIntent(final ArrayList<TetheringInterface> available,
+ final ArrayList<TetheringInterface> localOnly,
+ final ArrayList<TetheringInterface> tethered,
+ final ArrayList<TetheringInterface> errored) {
+ final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED);
+ bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+
+ bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, toIfaces(available));
+ bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, toIfaces(localOnly));
+ bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, toIfaces(tethered));
+ bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, toIfaces(errored));
+
+ return bcast;
+ }
+
+ private class StateReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context content, Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) return;
+
+ if (action.equals(UsbManager.ACTION_USB_STATE)) {
+ handleUsbAction(intent);
+ } else if (action.equals(CONNECTIVITY_ACTION)) {
+ handleConnectivityAction(intent);
+ } else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
+ handleWifiApAction(intent);
+ } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
+ handleWifiP2pAction(intent);
+ } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ mLog.log("OBSERVED configuration changed");
+ updateConfiguration();
+ } else if (action.equals(UserManager.ACTION_USER_RESTRICTIONS_CHANGED)) {
+ mLog.log("OBSERVED user restrictions changed");
+ handleUserRestrictionAction();
+ } else if (action.equals(ACTION_RESTRICT_BACKGROUND_CHANGED)) {
+ mLog.log("OBSERVED data saver changed");
+ handleDataSaverChanged();
+ } else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
+ untetherAll();
+ }
+ }
+
+ private void handleConnectivityAction(Intent intent) {
+ final NetworkInfo networkInfo =
+ (NetworkInfo) intent.getParcelableExtra(EXTRA_NETWORK_INFO);
+ if (networkInfo == null
+ || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED) {
+ return;
+ }
+
+ if (VDBG) Log.d(TAG, "Tethering got CONNECTIVITY_ACTION: " + networkInfo.toString());
+ mTetherMainSM.sendMessage(TetherMainSM.CMD_UPSTREAM_CHANGED);
+ }
+
+ private void handleUsbAction(Intent intent) {
+ final boolean usbConnected = intent.getBooleanExtra(USB_CONNECTED, false);
+ final boolean usbConfigured = intent.getBooleanExtra(USB_CONFIGURED, false);
+ final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false);
+ final boolean ncmEnabled = intent.getBooleanExtra(USB_FUNCTION_NCM, false);
+
+ mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s",
+ usbConnected, usbConfigured, rndisEnabled));
+
+ // There are three types of ACTION_USB_STATE:
+ //
+ // - DISCONNECTED (USB_CONNECTED and USB_CONFIGURED are 0)
+ // Meaning: USB connection has ended either because of
+ // software reset or hard unplug.
+ //
+ // - CONNECTED (USB_CONNECTED is 1, USB_CONFIGURED is 0)
+ // Meaning: the first stage of USB protocol handshake has
+ // occurred but it is not complete.
+ //
+ // - CONFIGURED (USB_CONNECTED and USB_CONFIGURED are 1)
+ // Meaning: the USB handshake is completely done and all the
+ // functions are ready to use.
+ //
+ // For more explanation, see b/62552150 .
+ if (!usbConnected && mRndisEnabled) {
+ // Turn off tethering if it was enabled and there is a disconnect.
+ tetherMatchingInterfaces(IpServer.STATE_AVAILABLE, TETHERING_USB);
+ mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB);
+ } else if (usbConfigured && rndisEnabled) {
+ // Tether if rndis is enabled and usb is configured.
+ final int state = getRequestedState(TETHERING_USB);
+ tetherMatchingInterfaces(state, TETHERING_USB);
+ } else if (usbConnected && ncmEnabled) {
+ final int state = getRequestedState(TETHERING_NCM);
+ tetherMatchingInterfaces(state, TETHERING_NCM);
+ }
+ mRndisEnabled = usbConfigured && rndisEnabled;
+ }
+
+ private void handleWifiApAction(Intent intent) {
+ final int curState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED);
+ final String ifname = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+ final int ipmode = intent.getIntExtra(EXTRA_WIFI_AP_MODE, IFACE_IP_MODE_UNSPECIFIED);
+
+ switch (curState) {
+ case WifiManager.WIFI_AP_STATE_ENABLING:
+ // We can see this state on the way to both enabled and failure states.
+ break;
+ case WifiManager.WIFI_AP_STATE_ENABLED:
+ enableWifiIpServingLocked(ifname, ipmode);
+ break;
+ case WifiManager.WIFI_AP_STATE_DISABLING:
+ // We can see this state on the way to disabled.
+ break;
+ case WifiManager.WIFI_AP_STATE_DISABLED:
+ case WifiManager.WIFI_AP_STATE_FAILED:
+ default:
+ disableWifiIpServingLocked(ifname, curState);
+ break;
+ }
+ }
+
+ private boolean isGroupOwner(WifiP2pGroup group) {
+ return group != null && group.isGroupOwner()
+ && !TextUtils.isEmpty(group.getInterface());
+ }
+
+ private void handleWifiP2pAction(Intent intent) {
+ if (mConfig.isWifiP2pLegacyTetheringMode()) return;
+
+ final WifiP2pInfo p2pInfo =
+ (WifiP2pInfo) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
+ final WifiP2pGroup group =
+ (WifiP2pGroup) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
+
+ if (VDBG) {
+ Log.d(TAG, "WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group);
+ }
+
+ // if no group is formed, bring it down if needed.
+ if (p2pInfo == null || !p2pInfo.groupFormed) {
+ disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface);
+ mWifiP2pTetherInterface = null;
+ return;
+ }
+
+ // If there is a group but the device is not the owner, bail out.
+ if (!isGroupOwner(group)) return;
+
+ // If already serving from the correct interface, nothing to do.
+ if (group.getInterface().equals(mWifiP2pTetherInterface)) return;
+
+ // If already serving from another interface, turn it down first.
+ if (!TextUtils.isEmpty(mWifiP2pTetherInterface)) {
+ mLog.w("P2P tethered interface " + mWifiP2pTetherInterface
+ + "is different from current interface "
+ + group.getInterface() + ", re-tether it");
+ disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface);
+ }
+
+ // Finally bring up serving on the new interface
+ mWifiP2pTetherInterface = group.getInterface();
+ enableWifiIpServingLocked(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+ }
+
+ private void handleUserRestrictionAction() {
+ mTetheringRestriction.onUserRestrictionsChanged();
+ }
+
+ private void handleDataSaverChanged() {
+ final ConnectivityManager connMgr = (ConnectivityManager) mContext.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ final boolean isDataSaverEnabled = connMgr.getRestrictBackgroundStatus()
+ != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
+
+ if (mDataSaverEnabled == isDataSaverEnabled) return;
+
+ mDataSaverEnabled = isDataSaverEnabled;
+ if (mDataSaverEnabled) {
+ untetherAll();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ SparseArray<TetheringRequestParcel> getActiveTetheringRequests() {
+ return mActiveTetheringRequests;
+ }
+
+ @VisibleForTesting
+ boolean isTetheringActive() {
+ return getTetheredIfaces().length > 0;
+ }
+
+ @VisibleForTesting
+ protected static class UserRestrictionActionListener {
+ private final UserManager mUserMgr;
+ private final Tethering mTethering;
+ private final TetheringNotificationUpdater mNotificationUpdater;
+ public boolean mDisallowTethering;
+
+ public UserRestrictionActionListener(@NonNull UserManager um, @NonNull Tethering tethering,
+ @NonNull TetheringNotificationUpdater updater) {
+ mUserMgr = um;
+ mTethering = tethering;
+ mNotificationUpdater = updater;
+ mDisallowTethering = false;
+ }
+
+ public void onUserRestrictionsChanged() {
+ // getUserRestrictions gets restriction for this process' user, which is the primary
+ // user. This is fine because DISALLOW_CONFIG_TETHERING can only be set on the primary
+ // user. See UserManager.DISALLOW_CONFIG_TETHERING.
+ final Bundle restrictions = mUserMgr.getUserRestrictions();
+ final boolean newlyDisallowed =
+ restrictions.getBoolean(UserManager.DISALLOW_CONFIG_TETHERING);
+ final boolean prevDisallowed = mDisallowTethering;
+ mDisallowTethering = newlyDisallowed;
+
+ final boolean tetheringDisallowedChanged = (newlyDisallowed != prevDisallowed);
+ if (!tetheringDisallowedChanged) {
+ return;
+ }
+
+ if (!newlyDisallowed) {
+ // Clear the restricted notification when user is allowed to have tethering
+ // function.
+ mNotificationUpdater.tetheringRestrictionLifted();
+ return;
+ }
+
+ if (mTethering.isTetheringActive()) {
+ // Restricted notification is shown when tethering function is disallowed on
+ // user's device.
+ mNotificationUpdater.notifyTetheringDisabledByRestriction();
+
+ // Untether from all downstreams since tethering is disallowed.
+ mTethering.untetherAll();
+ }
+ // TODO(b/148139325): send tetheringSupported on restriction change
+ }
+ }
+
+ private void disableWifiIpServingLockedCommon(int tetheringType, String ifname, int apState) {
+ mLog.log("Canceling WiFi tethering request -"
+ + " type=" + tetheringType
+ + " interface=" + ifname
+ + " state=" + apState);
+
+ if (!TextUtils.isEmpty(ifname)) {
+ final TetherState ts = mTetherStates.get(ifname);
+ if (ts != null) {
+ ts.ipServer.unwanted();
+ return;
+ }
+ }
+
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
+ if (ipServer.interfaceType() == tetheringType) {
+ ipServer.unwanted();
+ return;
+ }
+ }
+
+ mLog.log("Error disabling Wi-Fi IP serving; "
+ + (TextUtils.isEmpty(ifname) ? "no interface name specified"
+ : "specified interface: " + ifname));
+ }
+
+ private void disableWifiIpServingLocked(String ifname, int apState) {
+ // Regardless of whether we requested this transition, the AP has gone
+ // down. Don't try to tether again unless we're requested to do so.
+ // TODO: Remove this altogether, once Wi-Fi reliably gives us an
+ // interface name with every broadcast.
+ mWifiTetherRequested = false;
+
+ disableWifiIpServingLockedCommon(TETHERING_WIFI, ifname, apState);
+ }
+
+ private void disableWifiP2pIpServingLockedIfNeeded(String ifname) {
+ if (TextUtils.isEmpty(ifname)) return;
+
+ disableWifiIpServingLockedCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+ }
+
+ private void enableWifiIpServingLocked(String ifname, int wifiIpMode) {
+ // Map wifiIpMode values to IpServer.Callback serving states, inferring
+ // from mWifiTetherRequested as a final "best guess".
+ final int ipServingMode;
+ switch (wifiIpMode) {
+ case IFACE_IP_MODE_TETHERED:
+ ipServingMode = IpServer.STATE_TETHERED;
+ break;
+ case IFACE_IP_MODE_LOCAL_ONLY:
+ ipServingMode = IpServer.STATE_LOCAL_ONLY;
+ break;
+ default:
+ mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
+ return;
+ }
+
+ if (!TextUtils.isEmpty(ifname)) {
+ maybeTrackNewInterfaceLocked(ifname);
+ changeInterfaceState(ifname, ipServingMode);
+ } else {
+ mLog.e(String.format(
+ "Cannot enable IP serving in mode %s on missing interface name",
+ ipServingMode));
+ }
+ }
+
+ // TODO: Consider renaming to something more accurate in its description.
+ // This method:
+ // - allows requesting either tethering or local hotspot serving states
+ // - handles both enabling and disabling serving states
+ // - only tethers the first matching interface in listInterfaces()
+ // order of a given type
+ private void tetherMatchingInterfaces(int requestedState, int interfaceType) {
+ if (VDBG) {
+ Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")");
+ }
+
+ String[] ifaces = null;
+ try {
+ ifaces = mNetd.interfaceGetList();
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error listing Interfaces", e);
+ return;
+ }
+ String chosenIface = null;
+ if (ifaces != null) {
+ for (String iface : ifaces) {
+ if (ifaceNameToType(iface) == interfaceType) {
+ chosenIface = iface;
+ break;
+ }
+ }
+ }
+ if (chosenIface == null) {
+ Log.e(TAG, "could not find iface of type " + interfaceType);
+ return;
+ }
+
+ changeInterfaceState(chosenIface, requestedState);
+ }
+
+ private void changeInterfaceState(String ifname, int requestedState) {
+ final int result;
+ switch (requestedState) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ result = untether(ifname);
+ break;
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ result = tether(ifname, requestedState);
+ break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + requestedState);
+ return;
+ }
+ if (result != TETHER_ERROR_NO_ERROR) {
+ Log.e(TAG, "unable start or stop tethering on iface " + ifname);
+ return;
+ }
+ }
+
+ TetheringConfiguration getTetheringConfiguration() {
+ return mConfig;
+ }
+
+ boolean hasTetherableConfiguration() {
+ final TetheringConfiguration cfg = mConfig;
+ final boolean hasDownstreamConfiguration =
+ (cfg.tetherableUsbRegexs.length != 0)
+ || (cfg.tetherableWifiRegexs.length != 0)
+ || (cfg.tetherableBluetoothRegexs.length != 0);
+ final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
+ || cfg.chooseUpstreamAutomatically;
+
+ return hasDownstreamConfiguration && hasUpstreamConfiguration;
+ }
+
+ void setUsbTethering(boolean enable, IIntResultListener listener) {
+ mHandler.post(() -> {
+ try {
+ listener.onResult(setUsbTethering(enable));
+ } catch (RemoteException e) { }
+ });
+ }
+
+ private int setUsbTethering(boolean enable) {
+ if (VDBG) Log.d(TAG, "setUsbTethering(" + enable + ")");
+ UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+ if (usbManager == null) {
+ mLog.e("setUsbTethering: failed to get UsbManager!");
+ return TETHER_ERROR_SERVICE_UNAVAIL;
+ }
+ usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_RNDIS
+ : UsbManager.FUNCTION_NONE);
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ private int setNcmTethering(boolean enable) {
+ if (VDBG) Log.d(TAG, "setNcmTethering(" + enable + ")");
+ UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+ usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM : UsbManager.FUNCTION_NONE);
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ // TODO review API - figure out how to delete these entirely.
+ String[] getTetheredIfaces() {
+ ArrayList<String> list = new ArrayList<String>();
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.lastState == IpServer.STATE_TETHERED) {
+ list.add(mTetherStates.keyAt(i));
+ }
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
+ String[] getTetherableIfacesForTest() {
+ ArrayList<String> list = new ArrayList<String>();
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+ list.add(mTetherStates.keyAt(i));
+ }
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
+ private void logMessage(State state, int what) {
+ mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+ }
+
+ private boolean upstreamWanted() {
+ if (!mForwardedDownstreams.isEmpty()) return true;
+ return mWifiTetherRequested;
+ }
+
+ // Needed because the canonical source of upstream truth is just the
+ // upstream interface set, |mCurrentUpstreamIfaceSet|.
+ private boolean pertainsToCurrentUpstream(UpstreamNetworkState ns) {
+ if (ns != null && ns.linkProperties != null && mCurrentUpstreamIfaceSet != null) {
+ for (String ifname : ns.linkProperties.getAllInterfaceNames()) {
+ if (mCurrentUpstreamIfaceSet.ifnames.contains(ifname)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ class TetherMainSM extends StateMachine {
+ // an interface SM has requested Tethering/Local Hotspot
+ static final int EVENT_IFACE_SERVING_STATE_ACTIVE = BASE_MAIN_SM + 1;
+ // an interface SM has unrequested Tethering/Local Hotspot
+ static final int EVENT_IFACE_SERVING_STATE_INACTIVE = BASE_MAIN_SM + 2;
+ // upstream connection change - do the right thing
+ static final int CMD_UPSTREAM_CHANGED = BASE_MAIN_SM + 3;
+ // we don't have a valid upstream conn, check again after a delay
+ static final int CMD_RETRY_UPSTREAM = BASE_MAIN_SM + 4;
+ // Events from NetworkCallbacks that we process on the main state
+ // machine thread on behalf of the UpstreamNetworkMonitor.
+ static final int EVENT_UPSTREAM_CALLBACK = BASE_MAIN_SM + 5;
+ // we treated the error and want now to clear it
+ static final int CMD_CLEAR_ERROR = BASE_MAIN_SM + 6;
+ static final int EVENT_IFACE_UPDATE_LINKPROPERTIES = BASE_MAIN_SM + 7;
+ // Events from EntitlementManager to choose upstream again.
+ static final int EVENT_UPSTREAM_PERMISSION_CHANGED = BASE_MAIN_SM + 8;
+ private final State mInitialState;
+ private final State mTetherModeAliveState;
+
+ private final State mSetIpForwardingEnabledErrorState;
+ private final State mSetIpForwardingDisabledErrorState;
+ private final State mStartTetheringErrorState;
+ private final State mStopTetheringErrorState;
+ private final State mSetDnsForwardersErrorState;
+
+ // This list is a little subtle. It contains all the interfaces that currently are
+ // requesting tethering, regardless of whether these interfaces are still members of
+ // mTetherStates. This allows us to maintain the following predicates:
+ //
+ // 1) mTetherStates contains the set of all currently existing, tetherable, link state up
+ // interfaces.
+ // 2) mNotifyList contains all state machines that may have outstanding tethering state
+ // that needs to be torn down.
+ // 3) Use mNotifyList for predictable ordering order for ConnectedClientsTracker.
+ //
+ // Because we excise interfaces immediately from mTetherStates, we must maintain mNotifyList
+ // so that the garbage collector does not clean up the state machine before it has a chance
+ // to tear itself down.
+ private final ArrayList<IpServer> mNotifyList;
+ private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+ private final OffloadWrapper mOffload;
+
+ private static final int UPSTREAM_SETTLE_TIME_MS = 10000;
+
+ TetherMainSM(String name, Looper looper, TetheringDependencies deps) {
+ super(name, looper);
+
+ mInitialState = new InitialState();
+ mTetherModeAliveState = new TetherModeAliveState();
+ mSetIpForwardingEnabledErrorState = new SetIpForwardingEnabledErrorState();
+ mSetIpForwardingDisabledErrorState = new SetIpForwardingDisabledErrorState();
+ mStartTetheringErrorState = new StartTetheringErrorState();
+ mStopTetheringErrorState = new StopTetheringErrorState();
+ mSetDnsForwardersErrorState = new SetDnsForwardersErrorState();
+
+ addState(mInitialState);
+ addState(mTetherModeAliveState);
+ addState(mSetIpForwardingEnabledErrorState);
+ addState(mSetIpForwardingDisabledErrorState);
+ addState(mStartTetheringErrorState);
+ addState(mStopTetheringErrorState);
+ addState(mSetDnsForwardersErrorState);
+
+ mNotifyList = new ArrayList<>();
+ mIPv6TetheringCoordinator = deps.getIPv6TetheringCoordinator(mNotifyList, mLog);
+ mOffload = new OffloadWrapper();
+
+ setInitialState(mInitialState);
+ }
+
+ /**
+ * Returns all downstreams that are serving clients, regardless of they are actually
+ * tethered or localOnly. This must be called on the tethering thread (not thread-safe).
+ */
+ @NonNull
+ public List<IpServer> getAllDownstreams() {
+ return mNotifyList;
+ }
+
+ class InitialState extends State {
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE: {
+ final IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
+ handleInterfaceServingStateActive(message.arg1, who);
+ transitionTo(mTetherModeAliveState);
+ break;
+ }
+ case EVENT_IFACE_SERVING_STATE_INACTIVE: {
+ final IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
+ handleInterfaceServingStateInactive(who);
+ break;
+ }
+ case EVENT_IFACE_UPDATE_LINKPROPERTIES:
+ // Silently ignore these for now.
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+ }
+
+ protected boolean turnOnMainTetherSettings() {
+ final TetheringConfiguration cfg = mConfig;
+ try {
+ mNetd.ipfwdEnableForwarding(TAG);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e(e);
+ transitionTo(mSetIpForwardingEnabledErrorState);
+ return false;
+ }
+
+ // TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
+ // Legacy DHCP server is disabled if passed an empty ranges array
+ final String[] dhcpRanges = cfg.enableLegacyDhcpServer
+ ? cfg.legacyDhcpRanges : new String[0];
+ try {
+ NetdUtils.tetherStart(mNetd, true /** usingLegacyDnsProxy */, dhcpRanges);
+ } catch (RemoteException | ServiceSpecificException e) {
+ try {
+ // Stop and retry.
+ mNetd.tetherStop();
+ NetdUtils.tetherStart(mNetd, true /** usingLegacyDnsProxy */, dhcpRanges);
+ } catch (RemoteException | ServiceSpecificException ee) {
+ mLog.e(ee);
+ transitionTo(mStartTetheringErrorState);
+ return false;
+ }
+ }
+ mLog.log("SET main tether settings: ON");
+ return true;
+ }
+
+ protected boolean turnOffMainTetherSettings() {
+ try {
+ mNetd.tetherStop();
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e(e);
+ transitionTo(mStopTetheringErrorState);
+ return false;
+ }
+ try {
+ mNetd.ipfwdDisableForwarding(TAG);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e(e);
+ transitionTo(mSetIpForwardingDisabledErrorState);
+ return false;
+ }
+ transitionTo(mInitialState);
+ mLog.log("SET main tether settings: OFF");
+ return true;
+ }
+
+ protected void chooseUpstreamType(boolean tryCell) {
+ // We rebuild configuration on ACTION_CONFIGURATION_CHANGED, but we
+ // do not currently know how to watch for changes in DUN settings.
+ maybeDunSettingChanged();
+
+ final TetheringConfiguration config = mConfig;
+ final UpstreamNetworkState ns = (config.chooseUpstreamAutomatically)
+ ? mUpstreamNetworkMonitor.getCurrentPreferredUpstream()
+ : mUpstreamNetworkMonitor.selectPreferredUpstreamType(
+ config.preferredUpstreamIfaceTypes);
+
+ if (ns == null) {
+ if (tryCell) {
+ mUpstreamNetworkMonitor.setTryCell(true);
+ // We think mobile should be coming up; don't set a retry.
+ } else {
+ sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
+ }
+ } else if (!isCellular(ns)) {
+ mUpstreamNetworkMonitor.setTryCell(false);
+ }
+
+ setUpstreamNetwork(ns);
+ final Network newUpstream = (ns != null) ? ns.network : null;
+ if (mTetherUpstream != newUpstream) {
+ mTetherUpstream = newUpstream;
+ mUpstreamNetworkMonitor.setCurrentUpstream(mTetherUpstream);
+ reportUpstreamChanged(ns);
+ }
+ }
+
+ protected void setUpstreamNetwork(UpstreamNetworkState ns) {
+ InterfaceSet ifaces = null;
+ if (ns != null) {
+ // Find the interface with the default IPv4 route. It may be the
+ // interface described by linkProperties, or one of the interfaces
+ // stacked on top of it.
+ mLog.i("Looking for default routes on: " + ns.linkProperties);
+ ifaces = TetheringInterfaceUtils.getTetheringInterfaces(ns);
+ mLog.i("Found upstream interface(s): " + ifaces);
+ }
+
+ if (ifaces != null) {
+ setDnsForwarders(ns.network, ns.linkProperties);
+ }
+ notifyDownstreamsOfNewUpstreamIface(ifaces);
+ if (ns != null && pertainsToCurrentUpstream(ns)) {
+ // If we already have UpstreamNetworkState for this network update it immediately.
+ handleNewUpstreamNetworkState(ns);
+ } else if (mCurrentUpstreamIfaceSet == null) {
+ // There are no available upstream networks.
+ handleNewUpstreamNetworkState(null);
+ }
+ }
+
+ protected void setDnsForwarders(final Network network, final LinkProperties lp) {
+ // TODO: Set v4 and/or v6 DNS per available connectivity.
+ final Collection<InetAddress> dnses = lp.getDnsServers();
+ // TODO: Properly support the absence of DNS servers.
+ final String[] dnsServers;
+ if (dnses != null && !dnses.isEmpty()) {
+ dnsServers = new String[dnses.size()];
+ int i = 0;
+ for (InetAddress dns : dnses) {
+ dnsServers[i++] = dns.getHostAddress();
+ }
+ } else {
+ dnsServers = mConfig.defaultIPv4DNS;
+ }
+ final int netId = (network != null) ? network.getNetId() : NETID_UNSET;
+ try {
+ mNetd.tetherDnsSet(netId, dnsServers);
+ mLog.log(String.format(
+ "SET DNS forwarders: network=%s dnsServers=%s",
+ network, Arrays.toString(dnsServers)));
+ } catch (RemoteException | ServiceSpecificException e) {
+ // TODO: Investigate how this can fail and what exactly
+ // happens if/when such failures occur.
+ mLog.e("setting DNS forwarders failed, " + e);
+ transitionTo(mSetDnsForwardersErrorState);
+ }
+ }
+
+ protected void notifyDownstreamsOfNewUpstreamIface(InterfaceSet ifaces) {
+ mCurrentUpstreamIfaceSet = ifaces;
+ for (IpServer ipServer : mNotifyList) {
+ ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifaces);
+ }
+ }
+
+ protected void handleNewUpstreamNetworkState(UpstreamNetworkState ns) {
+ mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
+ mOffload.updateUpstreamNetworkState(ns);
+
+ // TODO: Delete all related offload rules which are using this upstream.
+ if (ns != null) {
+ // Add upstream index to the map. The upstream interface index is required while
+ // the conntrack event builds the offload rules.
+ mBpfCoordinator.addUpstreamIfindexToMap(ns.linkProperties);
+ }
+ }
+
+ private void handleInterfaceServingStateActive(int mode, IpServer who) {
+ if (mNotifyList.indexOf(who) < 0) {
+ mNotifyList.add(who);
+ mIPv6TetheringCoordinator.addActiveDownstream(who, mode);
+ }
+
+ if (mode == IpServer.STATE_TETHERED) {
+ // No need to notify OffloadController just yet as there are no
+ // "offload-able" prefixes to pass along. This will handled
+ // when the TISM informs Tethering of its LinkProperties.
+ mForwardedDownstreams.add(who);
+ } else {
+ mOffload.excludeDownstreamInterface(who.interfaceName());
+ mForwardedDownstreams.remove(who);
+ }
+
+ // If this is a Wi-Fi interface, notify WifiManager of the active serving state.
+ if (who.interfaceType() == TETHERING_WIFI) {
+ final WifiManager mgr = getWifiManager();
+ final String iface = who.interfaceName();
+ switch (mode) {
+ case IpServer.STATE_TETHERED:
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_TETHERED);
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_LOCAL_ONLY);
+ break;
+ default:
+ Log.wtf(TAG, "Unknown active serving mode: " + mode);
+ break;
+ }
+ }
+ }
+
+ private void handleInterfaceServingStateInactive(IpServer who) {
+ mNotifyList.remove(who);
+ mIPv6TetheringCoordinator.removeActiveDownstream(who);
+ mOffload.excludeDownstreamInterface(who.interfaceName());
+ mForwardedDownstreams.remove(who);
+ updateConnectedClients(null /* wifiClients */);
+
+ // If this is a Wi-Fi interface, tell WifiManager of any errors
+ // or the inactive serving state.
+ if (who.interfaceType() == TETHERING_WIFI) {
+ final WifiManager mgr = getWifiManager();
+ final String iface = who.interfaceName();
+ if (mgr == null) {
+ Log.wtf(TAG, "Skipping WifiManager notification about inactive tethering");
+ } else if (who.lastError() != TETHER_ERROR_NO_ERROR) {
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_CONFIGURATION_ERROR);
+ } else {
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_UNSPECIFIED);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
+ if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
+ mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
+ return;
+ }
+
+ final UpstreamNetworkState ns = (UpstreamNetworkState) o;
+ switch (arg1) {
+ case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
+ mPrivateAddressCoordinator.updateUpstreamPrefix(ns);
+ break;
+ case UpstreamNetworkMonitor.EVENT_ON_LOST:
+ mPrivateAddressCoordinator.removeUpstreamPrefix(ns.network);
+ break;
+ }
+
+ if (mConfig.chooseUpstreamAutomatically
+ && arg1 == UpstreamNetworkMonitor.EVENT_DEFAULT_SWITCHED) {
+ chooseUpstreamType(true);
+ return;
+ }
+
+ if (ns == null || !pertainsToCurrentUpstream(ns)) {
+ // TODO: In future, this is where upstream evaluation and selection
+ // could be handled for notifications which include sufficient data.
+ // For example, after CONNECTIVITY_ACTION listening is removed, here
+ // is where we could observe a Wi-Fi network becoming available and
+ // passing validation.
+ if (mCurrentUpstreamIfaceSet == null) {
+ // If we have no upstream interface, try to run through upstream
+ // selection again. If, for example, IPv4 connectivity has shown up
+ // after IPv6 (e.g., 464xlat became available) we want the chance to
+ // notice and act accordingly.
+ chooseUpstreamType(false);
+ }
+ return;
+ }
+
+ switch (arg1) {
+ case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
+ if (ns.network.equals(mTetherUpstream)) {
+ mNotificationUpdater.onUpstreamCapabilitiesChanged(ns.networkCapabilities);
+ }
+ handleNewUpstreamNetworkState(ns);
+ break;
+ case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
+ chooseUpstreamType(false);
+ break;
+ case UpstreamNetworkMonitor.EVENT_ON_LOST:
+ // TODO: Re-evaluate possible upstreams. Currently upstream
+ // reevaluation is triggered via received CONNECTIVITY_ACTION
+ // broadcasts that result in being passed a
+ // TetherMainSM.CMD_UPSTREAM_CHANGED.
+ handleNewUpstreamNetworkState(null);
+ break;
+ default:
+ mLog.e("Unknown arg1 value: " + arg1);
+ break;
+ }
+ }
+
+ class TetherModeAliveState extends State {
+ boolean mUpstreamWanted = false;
+ boolean mTryCell = true;
+
+ @Override
+ public void enter() {
+ // If turning on main tether settings fails, we have already
+ // transitioned to an error state; exit early.
+ if (!turnOnMainTetherSettings()) {
+ return;
+ }
+
+ mPrivateAddressCoordinator.maybeRemoveDeprecatedUpstreams();
+ mUpstreamNetworkMonitor.startObserveAllNetworks();
+
+ // TODO: De-duplicate with updateUpstreamWanted() below.
+ if (upstreamWanted()) {
+ mUpstreamWanted = true;
+ mOffload.start();
+ chooseUpstreamType(true);
+ mTryCell = false;
+ }
+
+ // TODO: Check the upstream interface if it is managed by BPF offload.
+ mBpfCoordinator.startPolling();
+ }
+
+ @Override
+ public void exit() {
+ mOffload.stop();
+ mUpstreamNetworkMonitor.stop();
+ notifyDownstreamsOfNewUpstreamIface(null);
+ handleNewUpstreamNetworkState(null);
+ if (mTetherUpstream != null) {
+ mTetherUpstream = null;
+ reportUpstreamChanged(null);
+ }
+ mBpfCoordinator.stopPolling();
+ }
+
+ private boolean updateUpstreamWanted() {
+ final boolean previousUpstreamWanted = mUpstreamWanted;
+ mUpstreamWanted = upstreamWanted();
+ if (mUpstreamWanted != previousUpstreamWanted) {
+ if (mUpstreamWanted) {
+ mOffload.start();
+ } else {
+ mOffload.stop();
+ }
+ }
+ return previousUpstreamWanted;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ boolean retValue = true;
+ switch (message.what) {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE: {
+ IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
+ handleInterfaceServingStateActive(message.arg1, who);
+ who.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED,
+ mCurrentUpstreamIfaceSet);
+ // If there has been a change and an upstream is now
+ // desired, kick off the selection process.
+ final boolean previousUpstreamWanted = updateUpstreamWanted();
+ if (!previousUpstreamWanted && mUpstreamWanted) {
+ chooseUpstreamType(true);
+ }
+ break;
+ }
+ case EVENT_IFACE_SERVING_STATE_INACTIVE: {
+ IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
+ handleInterfaceServingStateInactive(who);
+
+ if (mNotifyList.isEmpty()) {
+ // This transitions us out of TetherModeAliveState,
+ // either to InitialState or an error state.
+ turnOffMainTetherSettings();
+ break;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size()
+ + " live requests:");
+ for (IpServer o : mNotifyList) {
+ Log.d(TAG, " " + o);
+ }
+ }
+ // If there has been a change and an upstream is no
+ // longer desired, release any mobile requests.
+ final boolean previousUpstreamWanted = updateUpstreamWanted();
+ if (previousUpstreamWanted && !mUpstreamWanted) {
+ mUpstreamNetworkMonitor.setTryCell(false);
+ }
+ break;
+ }
+ case EVENT_IFACE_UPDATE_LINKPROPERTIES: {
+ final LinkProperties newLp = (LinkProperties) message.obj;
+ if (message.arg1 == IpServer.STATE_TETHERED) {
+ mOffload.updateDownstreamLinkProperties(newLp);
+ } else {
+ mOffload.excludeDownstreamInterface(newLp.getInterfaceName());
+ }
+ break;
+ }
+ case EVENT_UPSTREAM_PERMISSION_CHANGED:
+ case CMD_UPSTREAM_CHANGED:
+ updateUpstreamWanted();
+ if (!mUpstreamWanted) break;
+
+ // Need to try DUN immediately if Wi-Fi goes down.
+ chooseUpstreamType(true);
+ mTryCell = false;
+ break;
+ case CMD_RETRY_UPSTREAM:
+ updateUpstreamWanted();
+ if (!mUpstreamWanted) break;
+
+ chooseUpstreamType(mTryCell);
+ mTryCell = !mTryCell;
+ break;
+ case EVENT_UPSTREAM_CALLBACK: {
+ updateUpstreamWanted();
+ if (mUpstreamWanted) {
+ handleUpstreamNetworkMonitorCallback(message.arg1, message.obj);
+ }
+ break;
+ }
+ default:
+ retValue = false;
+ break;
+ }
+ return retValue;
+ }
+ }
+
+ class ErrorState extends State {
+ private int mErrorNotification;
+
+ @Override
+ public boolean processMessage(Message message) {
+ boolean retValue = true;
+ switch (message.what) {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE:
+ IpServer who = (IpServer) message.obj;
+ who.sendMessage(mErrorNotification);
+ break;
+ case CMD_CLEAR_ERROR:
+ mErrorNotification = TETHER_ERROR_NO_ERROR;
+ transitionTo(mInitialState);
+ break;
+ default:
+ retValue = false;
+ }
+ return retValue;
+ }
+
+ void notify(int msgType) {
+ mErrorNotification = msgType;
+ for (IpServer ipServer : mNotifyList) {
+ ipServer.sendMessage(msgType);
+ }
+ }
+
+ }
+
+ class SetIpForwardingEnabledErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in setIpForwardingEnabled");
+ notify(IpServer.CMD_IP_FORWARDING_ENABLE_ERROR);
+ }
+ }
+
+ class SetIpForwardingDisabledErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in setIpForwardingDisabled");
+ notify(IpServer.CMD_IP_FORWARDING_DISABLE_ERROR);
+ }
+ }
+
+ class StartTetheringErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in startTethering");
+ notify(IpServer.CMD_START_TETHERING_ERROR);
+ try {
+ mNetd.ipfwdDisableForwarding(TAG);
+ } catch (RemoteException | ServiceSpecificException e) { }
+ }
+ }
+
+ class StopTetheringErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in stopTethering");
+ notify(IpServer.CMD_STOP_TETHERING_ERROR);
+ try {
+ mNetd.ipfwdDisableForwarding(TAG);
+ } catch (RemoteException | ServiceSpecificException e) { }
+ }
+ }
+
+ class SetDnsForwardersErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in setDnsForwarders");
+ notify(IpServer.CMD_SET_DNS_FORWARDERS_ERROR);
+ try {
+ mNetd.tetherStop();
+ } catch (RemoteException | ServiceSpecificException e) { }
+ try {
+ mNetd.ipfwdDisableForwarding(TAG);
+ } catch (RemoteException | ServiceSpecificException e) { }
+ }
+ }
+
+ // A wrapper class to handle multiple situations where several calls to
+ // the OffloadController need to happen together.
+ //
+ // TODO: This suggests that the interface between OffloadController and
+ // Tethering is in need of improvement. Refactor these calls into the
+ // OffloadController implementation.
+ class OffloadWrapper {
+ public void start() {
+ final int status = mOffloadController.start() ? TETHER_HARDWARE_OFFLOAD_STARTED
+ : TETHER_HARDWARE_OFFLOAD_FAILED;
+ updateOffloadStatus(status);
+ sendOffloadExemptPrefixes();
+ }
+
+ public void stop() {
+ mOffloadController.stop();
+ updateOffloadStatus(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ }
+
+ public void updateUpstreamNetworkState(UpstreamNetworkState ns) {
+ mOffloadController.setUpstreamLinkProperties(
+ (ns != null) ? ns.linkProperties : null);
+ }
+
+ public void updateDownstreamLinkProperties(LinkProperties newLp) {
+ // Update the list of offload-exempt prefixes before adding
+ // new prefixes on downstream interfaces to the offload HAL.
+ sendOffloadExemptPrefixes();
+ mOffloadController.notifyDownstreamLinkProperties(newLp);
+ }
+
+ public void excludeDownstreamInterface(String ifname) {
+ // This and other interfaces may be in local-only hotspot mode;
+ // resend all local prefixes to the OffloadController.
+ sendOffloadExemptPrefixes();
+ mOffloadController.removeDownstreamInterface(ifname);
+ }
+
+ public void sendOffloadExemptPrefixes() {
+ sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
+ }
+
+ public void sendOffloadExemptPrefixes(final Set<IpPrefix> localPrefixes) {
+ // Add in well-known minimum set.
+ PrefixUtils.addNonForwardablePrefixes(localPrefixes);
+ // Add tragically hardcoded prefixes.
+ localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
+
+ // Maybe add prefixes or addresses for downstreams, depending on
+ // the IP serving mode of each.
+ for (IpServer ipServer : mNotifyList) {
+ final LinkProperties lp = ipServer.linkProperties();
+
+ switch (ipServer.servingMode()) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ // No usable LinkProperties in these states.
+ continue;
+ case IpServer.STATE_TETHERED:
+ // Only add IPv4 /32 and IPv6 /128 prefixes. The
+ // directly-connected prefixes will be sent as
+ // downstream "offload-able" prefixes.
+ for (LinkAddress addr : lp.getAllLinkAddresses()) {
+ final InetAddress ip = addr.getAddress();
+ if (ip.isLinkLocalAddress()) continue;
+ localPrefixes.add(PrefixUtils.ipAddressAsPrefix(ip));
+ }
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ // Add prefixes covering all local IPs.
+ localPrefixes.addAll(PrefixUtils.localPrefixesFrom(lp));
+ break;
+ }
+ }
+
+ mOffloadController.setLocalPrefixes(localPrefixes);
+ }
+
+ private void updateOffloadStatus(final int newStatus) {
+ if (newStatus == mOffloadStatus) return;
+
+ mOffloadStatus = newStatus;
+ reportOffloadStatusChanged(mOffloadStatus);
+ }
+ }
+ }
+
+ private void startTrackDefaultNetwork() {
+ mUpstreamNetworkMonitor.startTrackDefaultNetwork(mEntitlementMgr);
+ }
+
+ /** Get the latest value of the tethering entitlement check. */
+ void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
+ boolean showEntitlementUi) {
+ if (receiver == null) return;
+
+ mHandler.post(() -> {
+ mEntitlementMgr.requestLatestTetheringEntitlementResult(type, receiver,
+ showEntitlementUi);
+ });
+ }
+
+ /** Register tethering event callback */
+ void registerTetheringEventCallback(ITetheringEventCallback callback) {
+ final boolean hasListPermission =
+ hasCallingPermission(NETWORK_SETTINGS)
+ || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+ || hasCallingPermission(NETWORK_STACK);
+ mHandler.post(() -> {
+ mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
+ final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
+ parcel.tetheringSupported = isTetheringSupported();
+ parcel.upstreamNetwork = mTetherUpstream;
+ parcel.config = mConfig.toStableParcelable();
+ parcel.states =
+ mTetherStatesParcel != null ? mTetherStatesParcel : emptyTetherStatesParcel();
+ parcel.tetheredClients = hasListPermission
+ ? mConnectedClientsTracker.getLastTetheredClients()
+ : Collections.emptyList();
+ parcel.offloadStatus = mOffloadStatus;
+ try {
+ callback.onCallbackStarted(parcel);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ });
+ }
+
+ private TetherStatesParcel emptyTetherStatesParcel() {
+ final TetherStatesParcel parcel = new TetherStatesParcel();
+ parcel.availableList = new TetheringInterface[0];
+ parcel.tetheredList = new TetheringInterface[0];
+ parcel.localOnlyList = new TetheringInterface[0];
+ parcel.erroredIfaceList = new TetheringInterface[0];
+ parcel.lastErrorList = new int[0];
+
+ return parcel;
+ }
+
+ private boolean hasCallingPermission(@NonNull String permission) {
+ return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED;
+ }
+
+ /** Unregister tethering event callback */
+ void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
+ mHandler.post(() -> {
+ mTetheringEventCallbacks.unregister(callback);
+ });
+ }
+
+ private void reportUpstreamChanged(UpstreamNetworkState ns) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ final Network network = (ns != null) ? ns.network : null;
+ final NetworkCapabilities capabilities = (ns != null) ? ns.networkCapabilities : null;
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ mTetheringEventCallbacks.getBroadcastItem(i).onUpstreamChanged(network);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ // Need to notify capabilities change after upstream network changed because new network's
+ // capabilities should be checked every time.
+ mNotificationUpdater.onUpstreamCapabilitiesChanged(capabilities);
+ }
+
+ private void reportConfigurationChanged(TetheringConfigurationParcel config) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ mTetheringEventCallbacks.getBroadcastItem(i).onConfigurationChanged(config);
+ // TODO(b/148139325): send tetheringSupported on configuration change
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ }
+
+ private void reportTetherStateChanged(TetherStatesParcel states) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ mTetheringEventCallbacks.getBroadcastItem(i).onTetherStatesChanged(states);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ }
+
+ private void reportTetherClientsChanged(List<TetheredClient> clients) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ final CallbackCookie cookie =
+ (CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i);
+ if (!cookie.hasListClientsPermission) continue;
+ mTetheringEventCallbacks.getBroadcastItem(i).onTetherClientsChanged(clients);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ }
+
+ private void reportOffloadStatusChanged(final int status) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ mTetheringEventCallbacks.getBroadcastItem(i).onOffloadStatusChanged(status);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ }
+
+ // if ro.tether.denied = true we default to no tethering
+ // gservices could set the secure setting to 1 though to enable it on a build where it
+ // had previously been turned off.
+ boolean isTetheringSupported() {
+ final int defaultVal = mDeps.isTetheringDenied() ? 0 : 1;
+ final boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
+ final boolean tetherEnabledInSettings = tetherSupported
+ && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
+
+ return tetherEnabledInSettings && hasTetherableConfiguration()
+ && !isProvisioningNeededButUnavailable();
+ }
+
+ private void dumpBpf(IndentingPrintWriter pw) {
+ pw.println("BPF offload:");
+ pw.increaseIndent();
+ mBpfCoordinator.dump(pw);
+ pw.decreaseIndent();
+ }
+
+ void doDump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
+ // Binder.java closes the resource for us.
+ @SuppressWarnings("resource") final IndentingPrintWriter pw = new IndentingPrintWriter(
+ writer, " ");
+
+ if (argsContain(args, "bpf")) {
+ dumpBpf(pw);
+ return;
+ }
+
+ pw.println("Tethering:");
+ pw.increaseIndent();
+
+ pw.println("Configuration:");
+ pw.increaseIndent();
+ final TetheringConfiguration cfg = mConfig;
+ cfg.dump(pw);
+ pw.decreaseIndent();
+
+ pw.println("Entitlement:");
+ pw.increaseIndent();
+ mEntitlementMgr.dump(pw);
+ pw.decreaseIndent();
+
+ pw.println("Tether state:");
+ pw.increaseIndent();
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final String iface = mTetherStates.keyAt(i);
+ final TetherState tetherState = mTetherStates.valueAt(i);
+ pw.print(iface + " - ");
+
+ switch (tetherState.lastState) {
+ case IpServer.STATE_UNAVAILABLE:
+ pw.print("UnavailableState");
+ break;
+ case IpServer.STATE_AVAILABLE:
+ pw.print("AvailableState");
+ break;
+ case IpServer.STATE_TETHERED:
+ pw.print("TetheredState");
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ pw.print("LocalHotspotState");
+ break;
+ default:
+ pw.print("UnknownState");
+ break;
+ }
+ pw.println(" - lastError = " + tetherState.lastError);
+ }
+ pw.println("Upstream wanted: " + upstreamWanted());
+ pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
+ pw.decreaseIndent();
+
+ pw.println("Hardware offload:");
+ pw.increaseIndent();
+ mOffloadController.dump(pw);
+ pw.decreaseIndent();
+
+ dumpBpf(pw);
+
+ pw.println("Private address coordinator:");
+ pw.increaseIndent();
+ mPrivateAddressCoordinator.dump(pw);
+ pw.decreaseIndent();
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ if (argsContain(args, "--short")) {
+ pw.println("<log removed for brevity>");
+ } else {
+ mLog.dump(fd, pw, args);
+ }
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+
+ void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PERMISSION_GRANTED) {
+ writer.println("Permission Denial: can't dump.");
+ return;
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ // Don't crash the system if something in doDump throws an exception, but try to propagate
+ // the exception to the caller.
+ AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
+ mHandler.post(() -> {
+ try {
+ doDump(fd, writer, args);
+ } catch (RuntimeException e) {
+ exceptionRef.set(e);
+ }
+ latch.countDown();
+ });
+
+ try {
+ if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
+ return;
+ }
+ } catch (InterruptedException e) {
+ exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+ }
+
+ final RuntimeException e = exceptionRef.get();
+ if (e != null) throw e;
+ }
+
+ private static boolean argsContain(String[] args, String target) {
+ for (String arg : args) {
+ if (target.equals(arg)) return true;
+ }
+ return false;
+ }
+
+ private void updateConnectedClients(final List<WifiClient> wifiClients) {
+ if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
+ wifiClients)) {
+ reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
+ }
+ }
+
+ private IpServer.Callback makeControlCallback() {
+ return new IpServer.Callback() {
+ @Override
+ public void updateInterfaceState(IpServer who, int state, int lastError) {
+ notifyInterfaceStateChange(who, state, lastError);
+ }
+
+ @Override
+ public void updateLinkProperties(IpServer who, LinkProperties newLp) {
+ notifyLinkPropertiesChanged(who, newLp);
+ }
+
+ @Override
+ public void dhcpLeasesChanged() {
+ updateConnectedClients(null /* wifiClients */);
+ }
+
+ @Override
+ public void requestEnableTethering(int tetheringType, boolean enabled) {
+ enableTetheringInternal(tetheringType, enabled, null);
+ }
+ };
+ }
+
+ // TODO: Move into TetherMainSM.
+ private void notifyInterfaceStateChange(IpServer who, int state, int error) {
+ final String iface = who.interfaceName();
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.ipServer.equals(who)) {
+ tetherState.lastState = state;
+ tetherState.lastError = error;
+ } else {
+ if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+ }
+
+ mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
+
+ // If TetherMainSM is in ErrorState, TetherMainSM stays there.
+ // Thus we give a chance for TetherMainSM to recover to InitialState
+ // by sending CMD_CLEAR_ERROR
+ if (error == TETHER_ERROR_INTERNAL_ERROR) {
+ mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
+ }
+ int which;
+ switch (state) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
+ break;
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
+ break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + state);
+ return;
+ }
+ mTetherMainSM.sendMessage(which, state, 0, who);
+ sendTetherStateChangedBroadcast();
+ }
+
+ private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
+ final String iface = who.interfaceName();
+ final int state;
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.ipServer.equals(who)) {
+ state = tetherState.lastState;
+ } else {
+ mLog.log("got notification from stale iface " + iface);
+ return;
+ }
+
+ mLog.log(String.format(
+ "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
+ iface, IpServer.getStateString(state), newLp));
+ final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+ mTetherMainSM.sendMessage(which, state, 0, newLp);
+ }
+
+ private void maybeTrackNewInterfaceLocked(final String iface) {
+ // If we don't care about this type of interface, ignore.
+ final int interfaceType = ifaceNameToType(iface);
+ if (interfaceType == TETHERING_INVALID) {
+ mLog.log(iface + " is not a tetherable iface, ignoring");
+ return;
+ }
+
+ final PackageManager pm = mContext.getPackageManager();
+ if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
+ return;
+ }
+ if (interfaceType == TETHERING_WIFI_P2P
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+ mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+ return;
+ }
+
+ maybeTrackNewInterfaceLocked(iface, interfaceType);
+ }
+
+ private void maybeTrackNewInterfaceLocked(final String iface, int interfaceType) {
+ // If we have already started a TISM for this interface, skip.
+ if (mTetherStates.containsKey(iface)) {
+ mLog.log("active iface (" + iface + ") reported as added, ignoring");
+ return;
+ }
+
+ mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
+ final TetherState tetherState = new TetherState(
+ new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
+ makeControlCallback(), mConfig.enableLegacyDhcpServer,
+ mConfig.isBpfOffloadEnabled(), mPrivateAddressCoordinator,
+ mDeps.getIpServerDependencies()));
+ mTetherStates.put(iface, tetherState);
+ tetherState.ipServer.start();
+ }
+
+ private void stopTrackingInterfaceLocked(final String iface) {
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ mLog.log("attempting to remove unknown iface (" + iface + "), ignoring");
+ return;
+ }
+ tetherState.ipServer.stop();
+ mLog.log("removing TetheringInterfaceStateMachine for: " + iface);
+ mTetherStates.remove(iface);
+ }
+
+ private static String[] copy(String[] strarray) {
+ return Arrays.copyOf(strarray, strarray.length);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
new file mode 100644
index 0000000..2beeeb8
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -0,0 +1,547 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.content.Context.TELEPHONY_SERVICE;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.TetheringConfigurationParcel;
+import android.net.util.SharedLog;
+import android.provider.DeviceConfig;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.StringJoiner;
+
+/**
+ * A utility class to encapsulate the various tethering configuration elements.
+ *
+ * This configuration data includes elements describing upstream properties
+ * (preferred and required types of upstream connectivity as well as default
+ * DNS servers to use if none are available) and downstream properties (such
+ * as regular expressions use to match suitable downstream interfaces and the
+ * DHCPv4 ranges to use).
+ *
+ * @hide
+ */
+public class TetheringConfiguration {
+ private static final String TAG = TetheringConfiguration.class.getSimpleName();
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static final String TETHERING_MODULE_NAME = "com.android.tethering";
+
+ // Default ranges used for the legacy DHCP server.
+ // USB is 192.168.42.1 and 255.255.255.0
+ // Wifi is 192.168.43.1 and 255.255.255.0
+ // BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1
+ // with 255.255.255.0
+ // P2P is 192.168.49.1 and 255.255.255.0
+ private static final String[] LEGACY_DHCP_DEFAULT_RANGE = {
+ "192.168.42.2", "192.168.42.254", "192.168.43.2", "192.168.43.254",
+ "192.168.44.2", "192.168.44.254", "192.168.45.2", "192.168.45.254",
+ "192.168.46.2", "192.168.46.254", "192.168.47.2", "192.168.47.254",
+ "192.168.48.2", "192.168.48.254", "192.168.49.2", "192.168.49.254",
+ };
+
+ private static final String[] DEFAULT_IPV4_DNS = {"8.8.4.4", "8.8.8.8"};
+
+ /**
+ * Override enabling BPF offload configuration for tethering.
+ */
+ public static final String OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD =
+ "override_tether_enable_bpf_offload";
+
+ /**
+ * Use the old dnsmasq DHCP server for tethering instead of the framework implementation.
+ */
+ public static final String TETHER_ENABLE_LEGACY_DHCP_SERVER =
+ "tether_enable_legacy_dhcp_server";
+
+ public static final String USE_LEGACY_WIFI_P2P_DEDICATED_IP =
+ "use_legacy_wifi_p2p_dedicated_ip";
+
+ /**
+ * Flag use to enable select all prefix ranges feature.
+ * TODO: Remove this flag if there are no problems after M-2020-12 rolls out.
+ */
+ public static final String TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES =
+ "tether_enable_select_all_prefix_ranges";
+
+ /**
+ * Experiment flag to force choosing upstreams automatically.
+ *
+ * This setting is intended to help force-enable the feature on OEM devices that disabled it
+ * via resource overlays, and later noticed issues. To that end, it overrides
+ * config_tether_upstream_automatic when set to true.
+ *
+ * This flag is enabled if !=0 and less than the module APK version: see
+ * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices
+ * should just set config_tether_upstream_automatic to true instead.
+ */
+ public static final String TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION =
+ "tether_force_upstream_automatic_version";
+
+ /**
+ * Default value that used to periodic polls tether offload stats from tethering offload HAL
+ * to make the data warnings work.
+ */
+ public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+
+ public final String[] tetherableUsbRegexs;
+ public final String[] tetherableWifiRegexs;
+ public final String[] tetherableWigigRegexs;
+ public final String[] tetherableWifiP2pRegexs;
+ public final String[] tetherableBluetoothRegexs;
+ public final String[] tetherableNcmRegexs;
+ public final boolean isDunRequired;
+ public final boolean chooseUpstreamAutomatically;
+ public final Collection<Integer> preferredUpstreamIfaceTypes;
+ public final String[] legacyDhcpRanges;
+ public final String[] defaultIPv4DNS;
+ public final boolean enableLegacyDhcpServer;
+
+ public final String[] provisioningApp;
+ public final String provisioningAppNoUi;
+ public final int provisioningCheckPeriod;
+ public final String provisioningResponse;
+
+ public final int activeDataSubId;
+
+ private final int mOffloadPollInterval;
+ // TODO: Add to TetheringConfigurationParcel if required.
+ private final boolean mEnableBpfOffload;
+ private final boolean mEnableWifiP2pDedicatedIp;
+
+ private final boolean mEnableSelectAllPrefixRange;
+
+ public TetheringConfiguration(Context ctx, SharedLog log, int id) {
+ final SharedLog configLog = log.forSubComponent("config");
+
+ activeDataSubId = id;
+ Resources res = getResources(ctx, activeDataSubId);
+
+ tetherableUsbRegexs = getResourceStringArray(res, R.array.config_tether_usb_regexs);
+ tetherableNcmRegexs = getResourceStringArray(res, R.array.config_tether_ncm_regexs);
+ // TODO: Evaluate deleting this altogether now that Wi-Fi always passes
+ // us an interface name. Careful consideration needs to be given to
+ // implications for Settings and for provisioning checks.
+ tetherableWifiRegexs = getResourceStringArray(res, R.array.config_tether_wifi_regexs);
+ tetherableWigigRegexs = getResourceStringArray(res, R.array.config_tether_wigig_regexs);
+ tetherableWifiP2pRegexs = getResourceStringArray(
+ res, R.array.config_tether_wifi_p2p_regexs);
+ tetherableBluetoothRegexs = getResourceStringArray(
+ res, R.array.config_tether_bluetooth_regexs);
+
+ isDunRequired = checkDunRequired(ctx);
+
+ final boolean forceAutomaticUpstream = !SdkLevel.isAtLeastS()
+ && isFeatureEnabled(ctx, TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION);
+ chooseUpstreamAutomatically = forceAutomaticUpstream || getResourceBoolean(
+ res, R.bool.config_tether_upstream_automatic, false /** defaultValue */);
+ preferredUpstreamIfaceTypes = getUpstreamIfaceTypes(res, isDunRequired);
+
+ legacyDhcpRanges = getLegacyDhcpRanges(res);
+ defaultIPv4DNS = copy(DEFAULT_IPV4_DNS);
+ mEnableBpfOffload = getEnableBpfOffload(res);
+ enableLegacyDhcpServer = getEnableLegacyDhcpServer(res);
+
+ provisioningApp = getResourceStringArray(res, R.array.config_mobile_hotspot_provision_app);
+ provisioningAppNoUi = getResourceString(res,
+ R.string.config_mobile_hotspot_provision_app_no_ui);
+ provisioningCheckPeriod = getResourceInteger(res,
+ R.integer.config_mobile_hotspot_provision_check_period,
+ 0 /* No periodic re-check */);
+ provisioningResponse = getResourceString(res,
+ R.string.config_mobile_hotspot_provision_response);
+
+ mOffloadPollInterval = getResourceInteger(res,
+ R.integer.config_tether_offload_poll_interval,
+ DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+
+ mEnableWifiP2pDedicatedIp = getResourceBoolean(res,
+ R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip,
+ false /* defaultValue */);
+
+ // Flags should normally not be booleans, but this is a kill-switch flag that is only used
+ // to turn off the feature, so binary rollback problems do not apply.
+ mEnableSelectAllPrefixRange = getDeviceConfigBoolean(
+ TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES, true /* defaultValue */);
+
+ configLog.log(toString());
+ }
+
+ /** Check whether input interface belong to usb.*/
+ public boolean isUsb(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableUsbRegexs);
+ }
+
+ /** Check whether input interface belong to wifi.*/
+ public boolean isWifi(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableWifiRegexs);
+ }
+
+ /** Check whether input interface belong to wigig.*/
+ public boolean isWigig(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableWigigRegexs);
+ }
+
+ /** Check whether this interface is Wifi P2P interface. */
+ public boolean isWifiP2p(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableWifiP2pRegexs);
+ }
+
+ /** Check whether using legacy mode for wifi P2P. */
+ public boolean isWifiP2pLegacyTetheringMode() {
+ return (tetherableWifiP2pRegexs == null || tetherableWifiP2pRegexs.length == 0);
+ }
+
+ /** Check whether input interface belong to bluetooth.*/
+ public boolean isBluetooth(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableBluetoothRegexs);
+ }
+
+ /** Check if interface is ncm */
+ public boolean isNcm(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableNcmRegexs);
+ }
+
+ /** Check whether no ui entitlement application is available.*/
+ public boolean hasMobileHotspotProvisionApp() {
+ return !TextUtils.isEmpty(provisioningAppNoUi);
+ }
+
+ /** Check whether dedicated wifi p2p address is enabled. */
+ public boolean shouldEnableWifiP2pDedicatedIp() {
+ return mEnableWifiP2pDedicatedIp;
+ }
+
+ /** Does the dumping.*/
+ public void dump(PrintWriter pw) {
+ pw.print("activeDataSubId: ");
+ pw.println(activeDataSubId);
+
+ dumpStringArray(pw, "tetherableUsbRegexs", tetherableUsbRegexs);
+ dumpStringArray(pw, "tetherableWifiRegexs", tetherableWifiRegexs);
+ dumpStringArray(pw, "tetherableWifiP2pRegexs", tetherableWifiP2pRegexs);
+ dumpStringArray(pw, "tetherableBluetoothRegexs", tetherableBluetoothRegexs);
+ dumpStringArray(pw, "tetherableNcmRegexs", tetherableNcmRegexs);
+
+ pw.print("isDunRequired: ");
+ pw.println(isDunRequired);
+
+ pw.print("chooseUpstreamAutomatically: ");
+ pw.println(chooseUpstreamAutomatically);
+ pw.print("legacyPreredUpstreamIfaceTypes: ");
+ pw.println(Arrays.toString(toIntArray(preferredUpstreamIfaceTypes)));
+
+ dumpStringArray(pw, "legacyDhcpRanges", legacyDhcpRanges);
+ dumpStringArray(pw, "defaultIPv4DNS", defaultIPv4DNS);
+
+ pw.print("offloadPollInterval: ");
+ pw.println(mOffloadPollInterval);
+
+ dumpStringArray(pw, "provisioningApp", provisioningApp);
+ pw.print("provisioningAppNoUi: ");
+ pw.println(provisioningAppNoUi);
+
+ pw.print("enableBpfOffload: ");
+ pw.println(mEnableBpfOffload);
+
+ pw.print("enableLegacyDhcpServer: ");
+ pw.println(enableLegacyDhcpServer);
+
+ pw.print("enableWifiP2pDedicatedIp: ");
+ pw.println(mEnableWifiP2pDedicatedIp);
+
+ pw.print("mEnableSelectAllPrefixRange: ");
+ pw.println(mEnableSelectAllPrefixRange);
+ }
+
+ /** Returns the string representation of this object.*/
+ public String toString() {
+ final StringJoiner sj = new StringJoiner(" ");
+ sj.add(String.format("activeDataSubId:%d", activeDataSubId));
+ sj.add(String.format("tetherableUsbRegexs:%s", makeString(tetherableUsbRegexs)));
+ sj.add(String.format("tetherableWifiRegexs:%s", makeString(tetherableWifiRegexs)));
+ sj.add(String.format("tetherableWifiP2pRegexs:%s", makeString(tetherableWifiP2pRegexs)));
+ sj.add(String.format("tetherableBluetoothRegexs:%s",
+ makeString(tetherableBluetoothRegexs)));
+ sj.add(String.format("isDunRequired:%s", isDunRequired));
+ sj.add(String.format("chooseUpstreamAutomatically:%s", chooseUpstreamAutomatically));
+ sj.add(String.format("offloadPollInterval:%d", mOffloadPollInterval));
+ sj.add(String.format("preferredUpstreamIfaceTypes:%s",
+ toIntArray(preferredUpstreamIfaceTypes)));
+ sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
+ sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
+ sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload));
+ sj.add(String.format("enableLegacyDhcpServer:%s", enableLegacyDhcpServer));
+ return String.format("TetheringConfiguration{%s}", sj.toString());
+ }
+
+ private static void dumpStringArray(PrintWriter pw, String label, String[] values) {
+ pw.print(label);
+ pw.print(": ");
+
+ if (values != null) {
+ final StringJoiner sj = new StringJoiner(", ", "[", "]");
+ for (String value : values) sj.add(value);
+ pw.print(sj.toString());
+ } else {
+ pw.print("null");
+ }
+
+ pw.println();
+ }
+
+ private static String makeString(String[] strings) {
+ if (strings == null) return "null";
+ final StringJoiner sj = new StringJoiner(",", "[", "]");
+ for (String s : strings) sj.add(s);
+ return sj.toString();
+ }
+
+ /** Check whether dun is required. */
+ public static boolean checkDunRequired(Context ctx) {
+ final TelephonyManager tm = (TelephonyManager) ctx.getSystemService(TELEPHONY_SERVICE);
+ // TelephonyManager would uses the active data subscription, which should be the one used
+ // by tethering.
+ return (tm != null) ? tm.isTetheringApnRequired() : false;
+ }
+
+ public int getOffloadPollInterval() {
+ return mOffloadPollInterval;
+ }
+
+ public boolean isBpfOffloadEnabled() {
+ return mEnableBpfOffload;
+ }
+
+ public boolean isSelectAllPrefixRangeEnabled() {
+ return mEnableSelectAllPrefixRange;
+ }
+
+ private static Collection<Integer> getUpstreamIfaceTypes(Resources res, boolean dunRequired) {
+ final int[] ifaceTypes = res.getIntArray(R.array.config_tether_upstream_types);
+ final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length);
+ for (int i : ifaceTypes) {
+ switch (i) {
+ case TYPE_MOBILE:
+ case TYPE_MOBILE_HIPRI:
+ if (dunRequired) continue;
+ break;
+ case TYPE_MOBILE_DUN:
+ if (!dunRequired) continue;
+ break;
+ }
+ upstreamIfaceTypes.add(i);
+ }
+
+ // Fix up upstream interface types for DUN or mobile. NOTE: independent
+ // of the value of |dunRequired|, cell data of one form or another is
+ // *always* an upstream, regardless of the upstream interface types
+ // specified by configuration resources.
+ if (dunRequired) {
+ appendIfNotPresent(upstreamIfaceTypes, TYPE_MOBILE_DUN);
+ } else {
+ // Do not modify if a cellular interface type is already present in the
+ // upstream interface types. Add TYPE_MOBILE and TYPE_MOBILE_HIPRI if no
+ // cellular interface types are found in the upstream interface types.
+ // This preserves backwards compatibility and prevents the DUN and default
+ // mobile types incorrectly appearing together, which could happen on
+ // previous releases in the common case where checkDunRequired returned
+ // DUN_UNSPECIFIED.
+ if (!containsOneOf(upstreamIfaceTypes, TYPE_MOBILE, TYPE_MOBILE_HIPRI)) {
+ upstreamIfaceTypes.add(TYPE_MOBILE);
+ upstreamIfaceTypes.add(TYPE_MOBILE_HIPRI);
+ }
+ }
+
+ // Always make sure our good friend Ethernet is present.
+ // TODO: consider unilaterally forcing this at the front.
+ prependIfNotPresent(upstreamIfaceTypes, TYPE_ETHERNET);
+
+ return upstreamIfaceTypes;
+ }
+
+ private static boolean matchesDownstreamRegexs(String iface, String[] regexs) {
+ for (String regex : regexs) {
+ if (iface.matches(regex)) return true;
+ }
+ return false;
+ }
+
+ private static String[] getLegacyDhcpRanges(Resources res) {
+ final String[] fromResource = getResourceStringArray(res, R.array.config_tether_dhcp_range);
+ if ((fromResource.length > 0) && (fromResource.length % 2 == 0)) {
+ return fromResource;
+ }
+ return copy(LEGACY_DHCP_DEFAULT_RANGE);
+ }
+
+ private static String getResourceString(Resources res, final int resId) {
+ try {
+ return res.getString(resId);
+ } catch (Resources.NotFoundException e) {
+ return "";
+ }
+ }
+
+ private static boolean getResourceBoolean(Resources res, int resId, boolean defaultValue) {
+ try {
+ return res.getBoolean(resId);
+ } catch (Resources.NotFoundException e404) {
+ return defaultValue;
+ }
+ }
+
+ private static String[] getResourceStringArray(Resources res, int resId) {
+ try {
+ final String[] strArray = res.getStringArray(resId);
+ return (strArray != null) ? strArray : EMPTY_STRING_ARRAY;
+ } catch (Resources.NotFoundException e404) {
+ return EMPTY_STRING_ARRAY;
+ }
+ }
+
+ private static int getResourceInteger(Resources res, int resId, int defaultValue) {
+ try {
+ return res.getInteger(resId);
+ } catch (Resources.NotFoundException e404) {
+ return defaultValue;
+ }
+ }
+
+ private boolean getEnableBpfOffload(final Resources res) {
+ // Get BPF offload config
+ // Priority 1: Device config
+ // Priority 2: Resource config
+ // Priority 3: Default value
+ final boolean defaultValue = getResourceBoolean(
+ res, R.bool.config_tether_enable_bpf_offload, true /** default value */);
+
+ return getDeviceConfigBoolean(OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD, defaultValue);
+ }
+
+ private boolean getEnableLegacyDhcpServer(final Resources res) {
+ return getResourceBoolean(
+ res, R.bool.config_tether_enable_legacy_dhcp_server, false /** defaultValue */)
+ || getDeviceConfigBoolean(
+ TETHER_ENABLE_LEGACY_DHCP_SERVER, false /** defaultValue */);
+ }
+
+ private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ // Due to the limitation of static mock for testing, using #getDeviceConfigProperty instead
+ // of DeviceConfig#getBoolean. If using #getBoolean here, the test can't know that the
+ // returned boolean value comes from device config or default value (because of null
+ // property string). See the test case testBpfOffload{*} in TetheringConfigurationTest.java.
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ @VisibleForTesting
+ protected String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, name);
+ }
+
+ @VisibleForTesting
+ protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+ return DeviceConfigUtils.isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag,
+ TETHERING_MODULE_NAME, false /* defaultEnabled */);
+ }
+
+ private Resources getResources(Context ctx, int subId) {
+ if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ return getResourcesForSubIdWrapper(ctx, subId);
+ } else {
+ return ctx.getResources();
+ }
+ }
+
+ @VisibleForTesting
+ protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
+ return SubscriptionManager.getResourcesForSubId(ctx, subId);
+ }
+
+ private static String[] copy(String[] strarray) {
+ return Arrays.copyOf(strarray, strarray.length);
+ }
+
+ private static void prependIfNotPresent(ArrayList<Integer> list, int value) {
+ if (list.contains(value)) return;
+ list.add(0, value);
+ }
+
+ private static void appendIfNotPresent(ArrayList<Integer> list, int value) {
+ if (list.contains(value)) return;
+ list.add(value);
+ }
+
+ private static boolean containsOneOf(ArrayList<Integer> list, Integer... values) {
+ for (Integer value : values) {
+ if (list.contains(value)) return true;
+ }
+ return false;
+ }
+
+ private static int[] toIntArray(Collection<Integer> values) {
+ final int[] result = new int[values.size()];
+ int index = 0;
+ for (Integer value : values) {
+ result[index++] = value;
+ }
+ return result;
+ }
+
+ /**
+ * Convert this TetheringConfiguration to a TetheringConfigurationParcel.
+ */
+ public TetheringConfigurationParcel toStableParcelable() {
+ final TetheringConfigurationParcel parcel = new TetheringConfigurationParcel();
+ parcel.subId = activeDataSubId;
+ parcel.tetherableUsbRegexs = tetherableUsbRegexs;
+ parcel.tetherableWifiRegexs = tetherableWifiRegexs;
+ parcel.tetherableBluetoothRegexs = tetherableBluetoothRegexs;
+ parcel.isDunRequired = isDunRequired;
+ parcel.chooseUpstreamAutomatically = chooseUpstreamAutomatically;
+
+ parcel.preferredUpstreamIfaceTypes = toIntArray(preferredUpstreamIfaceTypes);
+
+ parcel.legacyDhcpRanges = legacyDhcpRanges;
+ parcel.defaultIPv4DNS = defaultIPv4DNS;
+ parcel.enableLegacyDhcpServer = enableLegacyDhcpServer;
+ parcel.provisioningApp = provisioningApp;
+ parcel.provisioningAppNoUi = provisioningAppNoUi;
+ parcel.provisioningCheckPeriod = provisioningCheckPeriod;
+ return parcel;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
new file mode 100644
index 0000000..7df9475
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import android.app.usage.NetworkStatsManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.net.INetd;
+import android.net.ip.IpServer;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.StateMachine;
+
+import java.util.ArrayList;
+
+
+/**
+ * Capture tethering dependencies, for injection.
+ *
+ * @hide
+ */
+public abstract class TetheringDependencies {
+ /**
+ * Get a reference to the BpfCoordinator to be used by tethering.
+ */
+ public @NonNull BpfCoordinator getBpfCoordinator(
+ @NonNull BpfCoordinator.Dependencies deps) {
+ return new BpfCoordinator(deps);
+ }
+
+ /**
+ * Get a reference to the offload hardware interface to be used by tethering.
+ */
+ public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+ return new OffloadHardwareInterface(h, log);
+ }
+
+ /**
+ * Get a reference to the offload controller to be used by tethering.
+ */
+ @NonNull
+ public OffloadController getOffloadController(@NonNull Handler h,
+ @NonNull SharedLog log, @NonNull OffloadController.Dependencies deps) {
+ final NetworkStatsManager statsManager =
+ (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE);
+ return new OffloadController(h, getOffloadHardwareInterface(h, log),
+ getContext().getContentResolver(), statsManager, log, deps);
+ }
+
+
+ /**
+ * Get a reference to the UpstreamNetworkMonitor to be used by tethering.
+ */
+ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
+ SharedLog log, int what) {
+ return new UpstreamNetworkMonitor(ctx, target, log, what);
+ }
+
+ /**
+ * Get a reference to the IPv6TetheringCoordinator to be used by tethering.
+ */
+ public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+ ArrayList<IpServer> notifyList, SharedLog log) {
+ return new IPv6TetheringCoordinator(notifyList, log);
+ }
+
+ /**
+ * Get dependencies to be used by IpServer.
+ */
+ public abstract IpServer.Dependencies getIpServerDependencies();
+
+ /**
+ * Indicates whether tethering is supported on the device.
+ */
+ public boolean isTetheringSupported() {
+ return true;
+ }
+
+ /**
+ * Get a reference to the EntitlementManager to be used by tethering.
+ */
+ public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
+ Runnable callback) {
+ return new EntitlementManager(ctx, h, log, callback);
+ }
+
+ /**
+ * Generate a new TetheringConfiguration according to input sub Id.
+ */
+ public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
+ int subId) {
+ return new TetheringConfiguration(ctx, log, subId);
+ }
+
+ /**
+ * 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));
+ }
+
+ /**
+ * Get a reference to the TetheringNotificationUpdater to be used by tethering.
+ */
+ public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
+ @NonNull final Looper looper) {
+ return new TetheringNotificationUpdater(ctx, looper);
+ }
+
+ /**
+ * Get tethering thread looper.
+ */
+ public abstract Looper getTetheringLooper();
+
+ /**
+ * Get Context of TetheringSerice.
+ */
+ public abstract Context getContext();
+
+ /**
+ * Get a reference to BluetoothAdapter to be used by tethering.
+ */
+ public abstract BluetoothAdapter getBluetoothAdapter();
+
+ /**
+ * Get SystemProperties which indicate whether tethering is denied.
+ */
+ public boolean isTetheringDenied() {
+ return TextUtils.equals(SystemProperties.get("ro.tether.denied"), "true");
+ }
+
+ /**
+ * Get a reference to PrivateAddressCoordinator to be used by Tethering.
+ */
+ public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
+ TetheringConfiguration cfg) {
+ return new PrivateAddressCoordinator(ctx, cfg);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java b/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java
new file mode 100644
index 0000000..ff38f71
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.RouteInfo;
+import android.net.util.InterfaceSet;
+
+import com.android.net.module.util.NetUtils;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * @hide
+ */
+public final class TetheringInterfaceUtils {
+ private static final InetAddress IN6ADDR_ANY = getByAddress(
+ new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+ private static final InetAddress INADDR_ANY = getByAddress(new byte[] {0, 0, 0, 0});
+
+ /**
+ * Get upstream interfaces for tethering based on default routes for IPv4/IPv6.
+ * @return null if there is no usable interface, or a set of at least one interface otherwise.
+ */
+ public static @Nullable InterfaceSet getTetheringInterfaces(UpstreamNetworkState ns) {
+ if (ns == null) {
+ return null;
+ }
+
+ final LinkProperties lp = ns.linkProperties;
+ final String if4 = getInterfaceForDestination(lp, INADDR_ANY);
+ final String if6 = getIPv6Interface(ns);
+
+ return (if4 == null && if6 == null) ? null : new InterfaceSet(if4, if6);
+ }
+
+ /**
+ * Get the upstream interface for IPv6 tethering.
+ * @return null if there is no usable interface, or the interface name otherwise.
+ */
+ public static @Nullable String getIPv6Interface(UpstreamNetworkState ns) {
+ // Broadly speaking:
+ //
+ // [1] does the upstream have an IPv6 default route?
+ //
+ // and
+ //
+ // [2] does the upstream have one or more global IPv6 /64s
+ // dedicated to this device?
+ //
+ // In lieu of Prefix Delegation and other evaluation of whether a
+ // prefix may or may not be dedicated to this device, for now just
+ // check whether the upstream is TRANSPORT_CELLULAR. This works
+ // because "[t]he 3GPP network allocates each default bearer a unique
+ // /64 prefix", per RFC 6459, Section 5.2.
+ final boolean canTether =
+ (ns != null) && (ns.network != null)
+ && (ns.linkProperties != null) && (ns.networkCapabilities != null)
+ // At least one upstream DNS server:
+ && ns.linkProperties.hasIpv6DnsServer()
+ // Minimal amount of IPv6 provisioning:
+ && ns.linkProperties.hasGlobalIpv6Address()
+ // Temporary approximation of "dedicated prefix":
+ && ns.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
+
+ return canTether
+ ? getInterfaceForDestination(ns.linkProperties, IN6ADDR_ANY)
+ : null;
+ }
+
+ private static String getInterfaceForDestination(LinkProperties lp, InetAddress dst) {
+ final RouteInfo ri = (lp != null)
+ ? NetUtils.selectBestRoute(lp.getAllRoutes(), dst)
+ : null;
+ return (ri != null) ? ri.getInterface() : null;
+ }
+
+ private static InetAddress getByAddress(final byte[] addr) {
+ try {
+ return InetAddress.getByAddress(null, addr);
+ } catch (UnknownHostException e) {
+ throw new AssertionError("illegal address length" + addr.length);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
new file mode 100644
index 0000000..a0198cc
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
@@ -0,0 +1,362 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.text.TextUtils.isEmpty;
+
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.SparseArray;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class to display tethering-related notifications.
+ *
+ * <p>This class is not thread safe, it is intended to be used only from the tethering handler
+ * thread. However the constructor is an exception, as it is called on another thread ;
+ * therefore for thread safety all members of this class MUST either be final or initialized
+ * to their default value (0, false or null).
+ *
+ * @hide
+ */
+public class TetheringNotificationUpdater {
+ private static final String TAG = TetheringNotificationUpdater.class.getSimpleName();
+ private static final String CHANNEL_ID = "TETHERING_STATUS";
+ private static final String WIFI_DOWNSTREAM = "WIFI";
+ private static final String USB_DOWNSTREAM = "USB";
+ private static final String BLUETOOTH_DOWNSTREAM = "BT";
+ @VisibleForTesting
+ static final String ACTION_DISABLE_TETHERING =
+ "com.android.server.connectivity.tethering.DISABLE_TETHERING";
+ private static final boolean NOTIFY_DONE = true;
+ private static final boolean NO_NOTIFY = false;
+ @VisibleForTesting
+ static final int EVENT_SHOW_NO_UPSTREAM = 1;
+ // Id to update and cancel restricted notification. Must be unique within the tethering app.
+ @VisibleForTesting
+ static final int RESTRICTED_NOTIFICATION_ID = 1001;
+ // Id to update and cancel no upstream notification. Must be unique within the tethering app.
+ @VisibleForTesting
+ static final int NO_UPSTREAM_NOTIFICATION_ID = 1002;
+ // Id to update and cancel roaming notification. Must be unique within the tethering app.
+ @VisibleForTesting
+ static final int ROAMING_NOTIFICATION_ID = 1003;
+ @VisibleForTesting
+ static final int NO_ICON_ID = 0;
+ @VisibleForTesting
+ static final int DOWNSTREAM_NONE = 0;
+ // Refer to TelephonyManager#getSimCarrierId for more details about carrier id.
+ @VisibleForTesting
+ static final int VERIZON_CARRIER_ID = 1839;
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ private final NotificationChannel mChannel;
+ private final Handler mHandler;
+
+ // WARNING : the constructor is called on a different thread. Thread safety therefore
+ // relies on these values being initialized to 0, false or null, and not any other value. If you
+ // need to change this, you will need to change the thread where the constructor is invoked, or
+ // to introduce synchronization.
+ // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
+ // This value has to be made 1 2 and 4, and OR'd with the others.
+ private int mDownstreamTypesMask = DOWNSTREAM_NONE;
+ private boolean mNoUpstream = false;
+ private boolean mRoaming = false;
+
+ // WARNING : this value is not able to being initialized to 0 and must have volatile because
+ // telephony service is not guaranteed that is up before tethering service starts. If telephony
+ // is up later than tethering, TetheringNotificationUpdater will use incorrect and valid
+ // subscription id(0) to query resources. Therefore, initialized subscription id must be
+ // INVALID_SUBSCRIPTION_ID.
+ private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ RESTRICTED_NOTIFICATION_ID,
+ NO_UPSTREAM_NOTIFICATION_ID,
+ ROAMING_NOTIFICATION_ID
+ })
+ @interface NotificationId {}
+
+ private static final class MccMncOverrideInfo {
+ public final String visitedMccMnc;
+ public final int homeMcc;
+ public final int homeMnc;
+ MccMncOverrideInfo(String visitedMccMnc, int mcc, int mnc) {
+ this.visitedMccMnc = visitedMccMnc;
+ this.homeMcc = mcc;
+ this.homeMnc = mnc;
+ }
+ }
+
+ private static final SparseArray<MccMncOverrideInfo> sCarrierIdToMccMnc = new SparseArray<>();
+
+ static {
+ sCarrierIdToMccMnc.put(VERIZON_CARRIER_ID, new MccMncOverrideInfo("20404", 311, 480));
+ }
+
+ public TetheringNotificationUpdater(@NonNull final Context context,
+ @NonNull final Looper looper) {
+ mContext = context;
+ mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mChannel = new NotificationChannel(
+ CHANNEL_ID,
+ context.getResources().getString(R.string.notification_channel_tethering_status),
+ NotificationManager.IMPORTANCE_LOW);
+ mNotificationManager.createNotificationChannel(mChannel);
+ mHandler = new NotificationHandler(looper);
+ }
+
+ private class NotificationHandler extends Handler {
+ NotificationHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case EVENT_SHOW_NO_UPSTREAM:
+ notifyTetheringNoUpstream();
+ break;
+ }
+ }
+ }
+
+ /** Called when downstream has changed */
+ public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) {
+ updateActiveNotifications(
+ mActiveDataSubId, downstreamTypesMask, mNoUpstream, mRoaming);
+ }
+
+ /** Called when active data subscription id changed */
+ public void onActiveDataSubscriptionIdChanged(final int subId) {
+ updateActiveNotifications(subId, mDownstreamTypesMask, mNoUpstream, mRoaming);
+ }
+
+ /** Called when upstream network capabilities changed */
+ public void onUpstreamCapabilitiesChanged(@Nullable final NetworkCapabilities capabilities) {
+ final boolean isNoUpstream = (capabilities == null);
+ final boolean isRoaming = capabilities != null
+ && !capabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+ updateActiveNotifications(
+ mActiveDataSubId, mDownstreamTypesMask, isNoUpstream, isRoaming);
+ }
+
+ @NonNull
+ @VisibleForTesting
+ final Handler getHandler() {
+ return mHandler;
+ }
+
+ @NonNull
+ @VisibleForTesting
+ Resources getResourcesForSubId(@NonNull final Context context, final int subId) {
+ final Resources res = SubscriptionManager.getResourcesForSubId(context, subId);
+ final TelephonyManager tm =
+ ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE))
+ .createForSubscriptionId(mActiveDataSubId);
+ final int carrierId = tm.getSimCarrierId();
+ final String mccmnc = tm.getSimOperator();
+ final MccMncOverrideInfo overrideInfo = sCarrierIdToMccMnc.get(carrierId);
+ if (overrideInfo != null && overrideInfo.visitedMccMnc.equals(mccmnc)) {
+ // Re-configure MCC/MNC value to specific carrier to get right resources.
+ final Configuration config = res.getConfiguration();
+ config.mcc = overrideInfo.homeMcc;
+ config.mnc = overrideInfo.homeMnc;
+ return context.createConfigurationContext(config).getResources();
+ }
+ return res;
+ }
+
+ private void updateActiveNotifications(final int subId, final int downstreamTypes,
+ final boolean noUpstream, final boolean isRoaming) {
+ final boolean tetheringActiveChanged =
+ (downstreamTypes == DOWNSTREAM_NONE) != (mDownstreamTypesMask == DOWNSTREAM_NONE);
+ final boolean subIdChanged = subId != mActiveDataSubId;
+ final boolean upstreamChanged = noUpstream != mNoUpstream;
+ final boolean roamingChanged = isRoaming != mRoaming;
+ final boolean updateAll = tetheringActiveChanged || subIdChanged;
+ mActiveDataSubId = subId;
+ mDownstreamTypesMask = downstreamTypes;
+ mNoUpstream = noUpstream;
+ mRoaming = isRoaming;
+
+ if (updateAll || upstreamChanged) updateNoUpstreamNotification();
+ if (updateAll || roamingChanged) updateRoamingNotification();
+ }
+
+ private void updateNoUpstreamNotification() {
+ final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
+
+ if (tetheringInactive || !mNoUpstream || setupNoUpstreamNotification() == NO_NOTIFY) {
+ clearNotification(NO_UPSTREAM_NOTIFICATION_ID);
+ mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM);
+ }
+ }
+
+ private void updateRoamingNotification() {
+ final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
+
+ if (tetheringInactive || !mRoaming || setupRoamingNotification() == NO_NOTIFY) {
+ clearNotification(ROAMING_NOTIFICATION_ID);
+ }
+ }
+
+ @VisibleForTesting
+ void tetheringRestrictionLifted() {
+ clearNotification(RESTRICTED_NOTIFICATION_ID);
+ }
+
+ private void clearNotification(@NotificationId final int id) {
+ mNotificationManager.cancel(null /* tag */, id);
+ }
+
+ @VisibleForTesting
+ static String getSettingsPackageName(@NonNull final PackageManager pm) {
+ final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS);
+ final ComponentName settingsComponent = settingsIntent.resolveActivity(pm);
+ return settingsComponent != null
+ ? settingsComponent.getPackageName() : "com.android.settings";
+ }
+
+ @VisibleForTesting
+ void notifyTetheringDisabledByRestriction() {
+ final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+ final String title = res.getString(R.string.disable_tether_notification_title);
+ final String message = res.getString(R.string.disable_tether_notification_message);
+ if (isEmpty(title) || isEmpty(message)) return;
+
+ final PendingIntent pi = PendingIntent.getActivity(
+ mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ new Intent(Settings.ACTION_TETHER_SETTINGS)
+ .setPackage(getSettingsPackageName(mContext.getPackageManager()))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ PendingIntent.FLAG_IMMUTABLE,
+ null /* options */);
+
+ showNotification(R.drawable.stat_sys_tether_general, title, message,
+ RESTRICTED_NOTIFICATION_ID, false /* ongoing */, pi, new Action[0]);
+ }
+
+ private void notifyTetheringNoUpstream() {
+ final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+ final String title = res.getString(R.string.no_upstream_notification_title);
+ final String message = res.getString(R.string.no_upstream_notification_message);
+ final String disableButton =
+ res.getString(R.string.no_upstream_notification_disable_button);
+ if (isEmpty(title) || isEmpty(message) || isEmpty(disableButton)) return;
+
+ final Intent intent = new Intent(ACTION_DISABLE_TETHERING);
+ intent.setPackage(mContext.getPackageName());
+ final PendingIntent pi = PendingIntent.getBroadcast(
+ mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ final Action action = new Action.Builder(NO_ICON_ID, disableButton, pi).build();
+
+ showNotification(R.drawable.stat_sys_tether_general, title, message,
+ NO_UPSTREAM_NOTIFICATION_ID, true /* ongoing */, null /* pendingIntent */, action);
+ }
+
+ private boolean setupRoamingNotification() {
+ final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+ final boolean upstreamRoamingNotification =
+ res.getBoolean(R.bool.config_upstream_roaming_notification);
+
+ if (!upstreamRoamingNotification) return NO_NOTIFY;
+
+ final String title = res.getString(R.string.upstream_roaming_notification_title);
+ final String message = res.getString(R.string.upstream_roaming_notification_message);
+ if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
+
+ final PendingIntent pi = PendingIntent.getActivity(
+ mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ new Intent(Settings.ACTION_TETHER_SETTINGS)
+ .setPackage(getSettingsPackageName(mContext.getPackageManager()))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ PendingIntent.FLAG_IMMUTABLE,
+ null /* options */);
+
+ showNotification(R.drawable.stat_sys_tether_general, title, message,
+ ROAMING_NOTIFICATION_ID, true /* ongoing */, pi, new Action[0]);
+ return NOTIFY_DONE;
+ }
+
+ private boolean setupNoUpstreamNotification() {
+ final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+ final int delayToShowUpstreamNotification =
+ res.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul);
+
+ if (delayToShowUpstreamNotification < 0) return NO_NOTIFY;
+
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_SHOW_NO_UPSTREAM),
+ delayToShowUpstreamNotification);
+ return NOTIFY_DONE;
+ }
+
+ private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
+ @NonNull final String message, @NotificationId final int id, final boolean ongoing,
+ @Nullable PendingIntent pi, @NonNull final Action... actions) {
+ final Notification notification =
+ new Notification.Builder(mContext, mChannel.getId())
+ .setSmallIcon(iconId)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setOngoing(ongoing)
+ .setColor(mContext.getColor(
+ android.R.color.system_notification_accent_color))
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setContentIntent(pi)
+ .setActions(actions)
+ .build();
+
+ mNotificationManager.notify(null /* tag */, id, notification);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
new file mode 100644
index 0000000..722ec8f
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -0,0 +1,371 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.content.Intent;
+import android.net.IIntResultListener;
+import android.net.INetworkStackConnector;
+import android.net.ITetheringConnector;
+import android.net.ITetheringEventCallback;
+import android.net.NetworkStack;
+import android.net.TetheringRequestParcel;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.ip.IpServer;
+import android.os.Binder;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.networkstack.apishim.SettingsShimImpl;
+import com.android.networkstack.apishim.common.SettingsShim;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Android service used to manage tethering.
+ *
+ * <p>The service returns a binder for the system server to communicate with the tethering.
+ */
+public class TetheringService extends Service {
+ private static final String TAG = TetheringService.class.getSimpleName();
+
+ private TetheringConnector mConnector;
+ private SettingsShim mSettingsShim;
+
+ @Override
+ public void onCreate() {
+ final TetheringDependencies deps = makeTetheringDependencies();
+ // The Tethering object needs a fully functional context to start, so this can't be done
+ // in the constructor.
+ mConnector = new TetheringConnector(makeTethering(deps), TetheringService.this);
+
+ mSettingsShim = SettingsShimImpl.newInstance();
+ }
+
+ /**
+ * Make a reference to Tethering object.
+ */
+ @VisibleForTesting
+ public Tethering makeTethering(TetheringDependencies deps) {
+ return new Tethering(deps);
+ }
+
+ @NonNull
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mConnector;
+ }
+
+ private static class TetheringConnector extends ITetheringConnector.Stub {
+ private final TetheringService mService;
+ private final Tethering mTethering;
+
+ TetheringConnector(Tethering tether, TetheringService service) {
+ mTethering = tether;
+ mService = service;
+ }
+
+ @Override
+ public void tether(String iface, String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+
+ mTethering.tether(iface, IpServer.STATE_TETHERED, listener);
+ }
+
+ @Override
+ public void untether(String iface, String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+
+ mTethering.untether(iface, listener);
+ }
+
+ @Override
+ public void setUsbTethering(boolean enable, String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+
+ mTethering.setUsbTethering(enable, listener);
+ }
+
+ @Override
+ public void startTethering(TetheringRequestParcel request, String callerPkg,
+ String callingAttributionTag, IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg,
+ callingAttributionTag,
+ request.exemptFromEntitlementCheck /* onlyAllowPrivileged */,
+ listener)) {
+ return;
+ }
+
+ mTethering.startTethering(request, listener);
+ }
+
+ @Override
+ public void stopTethering(int type, String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+
+ try {
+ mTethering.stopTethering(type);
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ } catch (RemoteException e) { }
+ }
+
+ @Override
+ public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
+ boolean showEntitlementUi, String callerPkg, String callingAttributionTag) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return;
+
+ mTethering.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
+ }
+
+ @Override
+ public void registerTetheringEventCallback(ITetheringEventCallback callback,
+ String callerPkg) {
+ try {
+ if (!hasTetherAccessPermission()) {
+ callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
+ return;
+ }
+ mTethering.registerTetheringEventCallback(callback);
+ } catch (RemoteException e) { }
+ }
+
+ @Override
+ public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
+ String callerPkg) {
+ try {
+ if (!hasTetherAccessPermission()) {
+ callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
+ return;
+ }
+ mTethering.unregisterTetheringEventCallback(callback);
+ } catch (RemoteException e) { }
+ }
+
+ @Override
+ public void stopAllTethering(String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+
+ try {
+ mTethering.untetherAll();
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ } catch (RemoteException e) { }
+ }
+
+ @Override
+ public void isTetheringSupported(String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+
+ try {
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ } catch (RemoteException e) { }
+ }
+
+ @Override
+ protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+ @Nullable String[] args) {
+ mTethering.dump(fd, writer, args);
+ }
+
+ private boolean checkAndNotifyCommonError(final String callerPkg,
+ final String callingAttributionTag, final IIntResultListener listener) {
+ return checkAndNotifyCommonError(callerPkg, callingAttributionTag,
+ false /* onlyAllowPrivileged */, listener);
+ }
+
+ private boolean checkAndNotifyCommonError(final String callerPkg,
+ final String callingAttributionTag, final boolean onlyAllowPrivileged,
+ final IIntResultListener listener) {
+ try {
+ if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
+ onlyAllowPrivileged)) {
+ listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ return true;
+ }
+ if (!mTethering.isTetheringSupported()) {
+ listener.onResult(TETHER_ERROR_UNSUPPORTED);
+ return true;
+ }
+ } catch (RemoteException e) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean checkAndNotifyCommonError(final String callerPkg,
+ final String callingAttributionTag, final ResultReceiver receiver) {
+ if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
+ false /* onlyAllowPrivileged */)) {
+ receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
+ return true;
+ }
+ if (!mTethering.isTetheringSupported()) {
+ receiver.send(TETHER_ERROR_UNSUPPORTED, null);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean hasNetworkStackPermission() {
+ return checkCallingOrSelfPermission(NETWORK_STACK)
+ || checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private boolean hasTetherPrivilegedPermission() {
+ return checkCallingOrSelfPermission(TETHER_PRIVILEGED);
+ }
+
+ private boolean checkCallingOrSelfPermission(final String permission) {
+ return mService.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED;
+ }
+
+ private boolean hasTetherChangePermission(final String callerPkg,
+ final String callingAttributionTag, final boolean onlyAllowPrivileged) {
+ if (onlyAllowPrivileged && !hasNetworkStackPermission()) return false;
+
+ if (hasTetherPrivilegedPermission()) return true;
+
+ if (mTethering.isTetherProvisioningRequired()) return false;
+
+ int uid = Binder.getCallingUid();
+
+ // If callerPkg's uid is not same as Binder.getCallingUid(),
+ // checkAndNoteWriteSettingsOperation will return false and the operation will be
+ // denied.
+ return mService.checkAndNoteWriteSettingsOperation(mService, uid, callerPkg,
+ callingAttributionTag, false /* throwException */);
+ }
+
+ private boolean hasTetherAccessPermission() {
+ if (hasTetherPrivilegedPermission()) return true;
+
+ return mService.checkCallingOrSelfPermission(
+ ACCESS_NETWORK_STATE) == PERMISSION_GRANTED;
+ }
+ }
+
+ /**
+ * Check if the package is a allowed to write settings. This also accounts that such an access
+ * happened.
+ *
+ * @return {@code true} iff the package is allowed to write settings.
+ */
+ @VisibleForTesting
+ boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
+ @NonNull String callingPackage, @Nullable String callingAttributionTag,
+ boolean throwException) {
+ return mSettingsShim.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,
+ callingAttributionTag, throwException);
+ }
+
+ /**
+ * An injection method for testing.
+ */
+ @VisibleForTesting
+ public TetheringDependencies makeTetheringDependencies() {
+ return new TetheringDependencies() {
+ @Override
+ public Looper getTetheringLooper() {
+ final HandlerThread tetherThread = new HandlerThread("android.tethering");
+ tetherThread.start();
+ return tetherThread.getLooper();
+ }
+
+ @Override
+ public Context getContext() {
+ return TetheringService.this;
+ }
+
+ @Override
+ public IpServer.Dependencies getIpServerDependencies() {
+ return new IpServer.Dependencies() {
+ @Override
+ public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+ DhcpServerCallbacks cb) {
+ try {
+ final INetworkStackConnector service = getNetworkStackConnector();
+ if (service == null) return;
+
+ service.makeDhcpServer(ifName, params, cb);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Fail to make dhcp server");
+ try {
+ cb.onDhcpServerCreated(STATUS_UNKNOWN_ERROR, null);
+ } catch (RemoteException re) { }
+ }
+ }
+ };
+ }
+
+ // TODO: replace this by NetworkStackClient#getRemoteConnector after refactoring
+ // networkStackClient.
+ static final int NETWORKSTACK_TIMEOUT_MS = 60_000;
+ private INetworkStackConnector getNetworkStackConnector() {
+ IBinder connector;
+ try {
+ final long before = System.currentTimeMillis();
+ while ((connector = NetworkStack.getService()) == null) {
+ if (System.currentTimeMillis() - before > NETWORKSTACK_TIMEOUT_MS) {
+ Log.wtf(TAG, "Timeout, fail to get INetworkStackConnector");
+ return null;
+ }
+ Thread.sleep(200);
+ }
+ } catch (InterruptedException e) {
+ Log.wtf(TAG, "Interrupted, fail to get INetworkStackConnector");
+ return null;
+ }
+ return INetworkStackConnector.Stub.asInterface(connector);
+ }
+
+ @Override
+ public BluetoothAdapter getBluetoothAdapter() {
+ return BluetoothAdapter.getDefaultAdapter();
+ }
+ };
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
new file mode 100644
index 0000000..e615334
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.util.PrefixUtils;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.StateMachine;
+import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+
+/**
+ * A class to centralize all the network and link properties information
+ * pertaining to the current and any potential upstream network.
+ *
+ * The owner of UNM gets it to register network callbacks by calling the
+ * following methods :
+ * Calling #startTrackDefaultNetwork() to track the system default network.
+ * Calling #startObserveAllNetworks() to observe all networks. Listening all
+ * networks is necessary while the expression of preferred upstreams remains
+ * a list of legacy connectivity types. In future, this can be revisited.
+ * Calling #setTryCell() to request bringing up mobile DUN or HIPRI.
+ *
+ * The methods and data members of this class are only to be accessed and
+ * modified from the tethering main state machine thread. Any other
+ * access semantics would necessitate the addition of locking.
+ *
+ * TODO: Move upstream selection logic here.
+ *
+ * All callback methods are run on the same thread as the specified target
+ * state machine. This class does not require locking when accessed from this
+ * thread. Access from other threads is not advised.
+ *
+ * @hide
+ */
+public class UpstreamNetworkMonitor {
+ private static final String TAG = UpstreamNetworkMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ public static final int EVENT_ON_CAPABILITIES = 1;
+ public static final int EVENT_ON_LINKPROPERTIES = 2;
+ public static final int EVENT_ON_LOST = 3;
+ public static final int EVENT_DEFAULT_SWITCHED = 4;
+ public static final int NOTIFY_LOCAL_PREFIXES = 10;
+ // This value is used by deprecated preferredUpstreamIfaceTypes selection which is default
+ // disabled.
+ @VisibleForTesting
+ public static final int TYPE_NONE = -1;
+
+ private static final int CALLBACK_LISTEN_ALL = 1;
+ private static final int CALLBACK_DEFAULT_INTERNET = 2;
+ private static final int CALLBACK_MOBILE_REQUEST = 3;
+
+ private static final SparseIntArray sLegacyTypeToTransport = new SparseIntArray();
+ static {
+ sLegacyTypeToTransport.put(TYPE_MOBILE, NetworkCapabilities.TRANSPORT_CELLULAR);
+ sLegacyTypeToTransport.put(TYPE_MOBILE_DUN, NetworkCapabilities.TRANSPORT_CELLULAR);
+ sLegacyTypeToTransport.put(TYPE_MOBILE_HIPRI, NetworkCapabilities.TRANSPORT_CELLULAR);
+ sLegacyTypeToTransport.put(TYPE_WIFI, NetworkCapabilities.TRANSPORT_WIFI);
+ sLegacyTypeToTransport.put(TYPE_BLUETOOTH, NetworkCapabilities.TRANSPORT_BLUETOOTH);
+ sLegacyTypeToTransport.put(TYPE_ETHERNET, NetworkCapabilities.TRANSPORT_ETHERNET);
+ }
+
+ private final Context mContext;
+ private final SharedLog mLog;
+ private final StateMachine mTarget;
+ private final Handler mHandler;
+ private final int mWhat;
+ private final HashMap<Network, UpstreamNetworkState> mNetworkMap = new HashMap<>();
+ private HashSet<IpPrefix> mLocalPrefixes;
+ private ConnectivityManager mCM;
+ private EntitlementManager mEntitlementMgr;
+ private NetworkCallback mListenAllCallback;
+ private NetworkCallback mDefaultNetworkCallback;
+ private NetworkCallback mMobileNetworkCallback;
+
+ /** Whether Tethering has requested a cellular upstream. */
+ private boolean mTryCell;
+ /** Whether the carrier requires DUN. */
+ private boolean mDunRequired;
+ /** Whether automatic upstream selection is enabled. */
+ private boolean mAutoUpstream;
+
+ // Whether the current default upstream is mobile or not.
+ private boolean mIsDefaultCellularUpstream;
+ // The current system default network (not really used yet).
+ private Network mDefaultInternetNetwork;
+ // The current upstream network used for tethering.
+ private Network mTetheringUpstreamNetwork;
+
+ public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
+ mContext = ctx;
+ mTarget = tgt;
+ mHandler = mTarget.getHandler();
+ mLog = log.forSubComponent(TAG);
+ mWhat = what;
+ mLocalPrefixes = new HashSet<>();
+ mIsDefaultCellularUpstream = false;
+ mCM = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ /**
+ * Tracking the system default network. This method should be only called once when system is
+ * ready, and the callback is never unregistered.
+ *
+ * @param entitle a EntitlementManager object to communicate between EntitlementManager and
+ * UpstreamNetworkMonitor
+ */
+ public void startTrackDefaultNetwork(EntitlementManager entitle) {
+ if (mDefaultNetworkCallback != null) {
+ Log.wtf(TAG, "default network callback is already registered");
+ return;
+ }
+ ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
+ mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
+ try {
+ mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
+ } catch (UnsupportedApiLevelException e) {
+ Log.wtf(TAG, "registerSystemDefaultNetworkCallback is not supported");
+ return;
+ }
+ if (mEntitlementMgr == null) {
+ mEntitlementMgr = entitle;
+ }
+ }
+
+ /** Listen all networks. */
+ public void startObserveAllNetworks() {
+ stop();
+
+ final NetworkRequest listenAllRequest = new NetworkRequest.Builder()
+ .clearCapabilities().build();
+ mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL);
+ cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler);
+ }
+
+ /**
+ * Stop tracking candidate tethering upstreams and release mobile network request.
+ * Note: this function is used when tethering is stopped because tethering do not need to
+ * choose upstream anymore. But it would not stop default network tracking because
+ * EntitlementManager may need to know default network to decide whether to request entitlement
+ * check even tethering is not active yet.
+ */
+ public void stop() {
+ releaseMobileNetworkRequest();
+
+ releaseCallback(mListenAllCallback);
+ mListenAllCallback = null;
+
+ mTetheringUpstreamNetwork = null;
+ mNetworkMap.clear();
+ }
+
+ private void reevaluateUpstreamRequirements(boolean tryCell, boolean autoUpstream,
+ boolean dunRequired) {
+ final boolean mobileRequestRequired = tryCell && (dunRequired || !autoUpstream);
+ final boolean dunRequiredChanged = (mDunRequired != dunRequired);
+
+ mTryCell = tryCell;
+ mDunRequired = dunRequired;
+ mAutoUpstream = autoUpstream;
+
+ if (mobileRequestRequired && !mobileNetworkRequested()) {
+ registerMobileNetworkRequest();
+ } else if (mobileNetworkRequested() && !mobileRequestRequired) {
+ releaseMobileNetworkRequest();
+ } else if (mobileNetworkRequested() && dunRequiredChanged) {
+ releaseMobileNetworkRequest();
+ if (mobileRequestRequired) {
+ registerMobileNetworkRequest();
+ }
+ }
+ }
+
+ /**
+ * Informs UpstreamNetworkMonitor that a cellular upstream is desired.
+ *
+ * This may result in filing a NetworkRequest for DUN if it is required, or for MOBILE_HIPRI if
+ * automatic upstream selection is disabled and MOBILE_HIPRI is the preferred upstream.
+ */
+ public void setTryCell(boolean tryCell) {
+ reevaluateUpstreamRequirements(tryCell, mAutoUpstream, mDunRequired);
+ }
+
+ /** Informs UpstreamNetworkMonitor of upstream configuration parameters. */
+ public void setUpstreamConfig(boolean autoUpstream, boolean dunRequired) {
+ reevaluateUpstreamRequirements(mTryCell, autoUpstream, dunRequired);
+ }
+
+ /** Whether mobile network is requested. */
+ public boolean mobileNetworkRequested() {
+ return (mMobileNetworkCallback != null);
+ }
+
+ /** Request mobile network if mobile upstream is permitted. */
+ private void registerMobileNetworkRequest() {
+ if (!isCellularUpstreamPermitted()) {
+ mLog.i("registerMobileNetworkRequest() is not permitted");
+ releaseMobileNetworkRequest();
+ return;
+ }
+ if (mMobileNetworkCallback != null) {
+ mLog.e("registerMobileNetworkRequest() already registered");
+ return;
+ }
+
+ final NetworkRequest mobileUpstreamRequest;
+ if (mDunRequired) {
+ mobileUpstreamRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_DUN)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ } else {
+ mobileUpstreamRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ }
+
+ // The existing default network and DUN callbacks will be notified.
+ // Therefore, to avoid duplicate notifications, we only register a no-op.
+ mMobileNetworkCallback = new UpstreamNetworkCallback(CALLBACK_MOBILE_REQUEST);
+
+ // The following use of the legacy type system cannot be removed until
+ // upstream selection no longer finds networks by legacy type.
+ // See also http://b/34364553 .
+ final int legacyType = mDunRequired ? TYPE_MOBILE_DUN : TYPE_MOBILE_HIPRI;
+
+ // TODO: Change the timeout from 0 (no onUnavailable callback) to some
+ // moderate callback timeout. This might be useful for updating some UI.
+ // Additionally, we log a message to aid in any subsequent debugging.
+ mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest
+ + " mTryCell=" + mTryCell + " mAutoUpstream=" + mAutoUpstream
+ + " mDunRequired=" + mDunRequired);
+
+ cm().requestNetwork(mobileUpstreamRequest, 0, legacyType, mHandler,
+ mMobileNetworkCallback);
+ }
+
+ /** Release mobile network request. */
+ private void releaseMobileNetworkRequest() {
+ if (mMobileNetworkCallback == null) return;
+
+ cm().unregisterNetworkCallback(mMobileNetworkCallback);
+ mMobileNetworkCallback = null;
+ }
+
+ // So many TODOs here, but chief among them is: make this functionality an
+ // integral part of this class such that whenever a higher priority network
+ // becomes available and useful we (a) file a request to keep it up as
+ // necessary and (b) change all upstream tracking state accordingly (by
+ // passing LinkProperties up to Tethering).
+ /**
+ * Select the first available network from |perferredTypes|.
+ */
+ public UpstreamNetworkState selectPreferredUpstreamType(Iterable<Integer> preferredTypes) {
+ final TypeStatePair typeStatePair = findFirstAvailableUpstreamByType(
+ mNetworkMap.values(), preferredTypes, isCellularUpstreamPermitted());
+
+ mLog.log("preferred upstream type: " + typeStatePair.type);
+
+ switch (typeStatePair.type) {
+ case TYPE_MOBILE_DUN:
+ case TYPE_MOBILE_HIPRI:
+ // Tethering just selected mobile upstream in spite of the default network being
+ // not mobile. This can happen because of the priority list.
+ // Notify EntitlementManager to check permission for using mobile upstream.
+ if (!mIsDefaultCellularUpstream) {
+ mEntitlementMgr.maybeRunProvisioning();
+ }
+ break;
+ }
+
+ return typeStatePair.ns;
+ }
+
+ /**
+ * Get current preferred upstream network. If default network is cellular and DUN is required,
+ * preferred upstream would be DUN otherwise preferred upstream is the same as default network.
+ * Returns null if no current upstream is available.
+ */
+ public UpstreamNetworkState getCurrentPreferredUpstream() {
+ final UpstreamNetworkState dfltState = (mDefaultInternetNetwork != null)
+ ? mNetworkMap.get(mDefaultInternetNetwork)
+ : null;
+ if (isNetworkUsableAndNotCellular(dfltState)) return dfltState;
+
+ if (!isCellularUpstreamPermitted()) return null;
+
+ if (!mDunRequired) return dfltState;
+
+ // Find a DUN network. Note that code in Tethering causes a DUN request
+ // to be filed, but this might be moved into this class in future.
+ return findFirstDunNetwork(mNetworkMap.values());
+ }
+
+ /** Tell UpstreamNetworkMonitor which network is the current upstream of tethering. */
+ public void setCurrentUpstream(Network upstream) {
+ mTetheringUpstreamNetwork = upstream;
+ }
+
+ /** Return local prefixes. */
+ public Set<IpPrefix> getLocalPrefixes() {
+ return (Set<IpPrefix>) mLocalPrefixes.clone();
+ }
+
+ private boolean isCellularUpstreamPermitted() {
+ if (mEntitlementMgr != null) {
+ return mEntitlementMgr.isCellularUpstreamPermitted();
+ } else {
+ // This flow should only happens in testing.
+ return true;
+ }
+ }
+
+ private void handleAvailable(Network network) {
+ if (mNetworkMap.containsKey(network)) return;
+
+ if (VDBG) Log.d(TAG, "onAvailable for " + network);
+ mNetworkMap.put(network, new UpstreamNetworkState(null, null, network));
+ }
+
+ private void handleNetCap(Network network, NetworkCapabilities newNc) {
+ final UpstreamNetworkState prev = mNetworkMap.get(network);
+ if (prev == null || newNc.equals(prev.networkCapabilities)) {
+ // Ignore notifications about networks for which we have not yet
+ // received onAvailable() (should never happen) and any duplicate
+ // notifications (e.g. matching more than one of our callbacks).
+ return;
+ }
+
+ if (VDBG) {
+ Log.d(TAG, String.format("EVENT_ON_CAPABILITIES for %s: %s",
+ network, newNc));
+ }
+
+ mNetworkMap.put(network, new UpstreamNetworkState(
+ prev.linkProperties, newNc, network));
+ // TODO: If sufficient information is available to select a more
+ // preferable upstream, do so now and notify the target.
+ notifyTarget(EVENT_ON_CAPABILITIES, network);
+ }
+
+ private @Nullable UpstreamNetworkState updateLinkProperties(@NonNull Network network,
+ LinkProperties newLp) {
+ final UpstreamNetworkState prev = mNetworkMap.get(network);
+ if (prev == null || newLp.equals(prev.linkProperties)) {
+ // Ignore notifications about networks for which we have not yet
+ // received onAvailable() (should never happen) and any duplicate
+ // notifications (e.g. matching more than one of our callbacks).
+ //
+ // Also, it can happen that onLinkPropertiesChanged is called after
+ // onLost removed the state from mNetworkMap. This appears to be due
+ // to a bug in disconnectAndDestroyNetwork, which calls
+ // nai.clatd.update() after the onLost callbacks.
+ // TODO: fix the bug and make this method void.
+ return null;
+ }
+
+ if (VDBG) {
+ Log.d(TAG, String.format("EVENT_ON_LINKPROPERTIES for %s: %s",
+ network, newLp));
+ }
+
+ final UpstreamNetworkState ns = new UpstreamNetworkState(newLp, prev.networkCapabilities,
+ network);
+ mNetworkMap.put(network, ns);
+ return ns;
+ }
+
+ private void handleLinkProp(Network network, LinkProperties newLp) {
+ final UpstreamNetworkState ns = updateLinkProperties(network, newLp);
+ if (ns != null) {
+ notifyTarget(EVENT_ON_LINKPROPERTIES, ns);
+ }
+ }
+
+ private void handleLost(Network network) {
+ // There are few TODOs within ConnectivityService's rematching code
+ // pertaining to spurious onLost() notifications.
+ //
+ // TODO: simplify this, probably if favor of code that:
+ // - selects a new upstream if mTetheringUpstreamNetwork has
+ // been lost (by any callback)
+ // - deletes the entry from the map only when the LISTEN_ALL
+ // callback gets notified.
+
+ if (!mNetworkMap.containsKey(network)) {
+ // Ignore loss of networks about which we had not previously
+ // learned any information or for which we have already processed
+ // an onLost() notification.
+ return;
+ }
+
+ if (VDBG) Log.d(TAG, "EVENT_ON_LOST for " + network);
+
+ // TODO: If sufficient information is available to select a more
+ // preferable upstream, do so now and notify the target. Likewise,
+ // if the current upstream network is gone, notify the target of the
+ // fact that we now have no upstream at all.
+ notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
+ }
+
+ private void maybeHandleNetworkSwitch(@NonNull Network network) {
+ if (Objects.equals(mDefaultInternetNetwork, network)) return;
+
+ final UpstreamNetworkState ns = mNetworkMap.get(network);
+ if (ns == null) {
+ // Can never happen unless there is a bug in ConnectivityService. Entries are only
+ // removed from mNetworkMap when receiving onLost, and onLost for a given network can
+ // never be followed by any other callback on that network.
+ Log.wtf(TAG, "maybeHandleNetworkSwitch: no UpstreamNetworkState for " + network);
+ return;
+ }
+
+ // Default network changed. Update local data and notify tethering.
+ Log.d(TAG, "New default Internet network: " + network);
+ mDefaultInternetNetwork = network;
+ notifyTarget(EVENT_DEFAULT_SWITCHED, ns);
+ }
+
+ private void recomputeLocalPrefixes() {
+ final HashSet<IpPrefix> localPrefixes = allLocalPrefixes(mNetworkMap.values());
+ if (!mLocalPrefixes.equals(localPrefixes)) {
+ mLocalPrefixes = localPrefixes;
+ notifyTarget(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
+ }
+ }
+
+ // Fetch (and cache) a ConnectivityManager only if and when we need one.
+ private ConnectivityManager cm() {
+ if (mCM == null) {
+ // MUST call the String variant to be able to write unittests.
+ mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+ return mCM;
+ }
+
+ /**
+ * A NetworkCallback class that handles information of interest directly
+ * in the thread on which it is invoked. To avoid locking, this MUST be
+ * run on the same thread as the target state machine's handler.
+ */
+ private class UpstreamNetworkCallback extends NetworkCallback {
+ private final int mCallbackType;
+
+ UpstreamNetworkCallback(int callbackType) {
+ mCallbackType = callbackType;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ handleAvailable(network);
+ }
+
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities newNc) {
+ if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
+ // mDefaultInternetNetwork is not updated here because upstream selection must only
+ // run when the LinkProperties have been updated as well as the capabilities. If
+ // this callback is due to a default network switch, then the system will invoke
+ // onLinkPropertiesChanged right after this method and mDefaultInternetNetwork will
+ // be updated then.
+ //
+ // Technically, not updating here isn't necessary, because the notifications to
+ // Tethering sent by notifyTarget are messages sent to a state machine running on
+ // the same thread as this method, and so cannot arrive until after this method has
+ // returned. However, it is not a good idea to rely on that because fact that
+ // Tethering uses multiple state machines running on the same thread is a major
+ // source of race conditions and something that should be fixed.
+ //
+ // TODO: is it correct that this code always updates EntitlementManager?
+ // This code runs when the default network connects or changes capabilities, but the
+ // default network might not be the tethering upstream.
+ final boolean newIsCellular = isCellular(newNc);
+ if (mIsDefaultCellularUpstream != newIsCellular) {
+ mIsDefaultCellularUpstream = newIsCellular;
+ mEntitlementMgr.notifyUpstream(newIsCellular);
+ }
+ return;
+ }
+
+ handleNetCap(network, newNc);
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
+ if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
+ updateLinkProperties(network, newLp);
+ // When the default network callback calls onLinkPropertiesChanged, it means that
+ // all the network information for the default network is known (because
+ // onLinkPropertiesChanged is called after onAvailable and onCapabilitiesChanged).
+ // Inform tethering that the default network might have changed.
+ maybeHandleNetworkSwitch(network);
+ return;
+ }
+
+ handleLinkProp(network, newLp);
+ // Any non-LISTEN_ALL callback will necessarily concern a network that will
+ // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
+ // So it's not useful to do this work for non-LISTEN_ALL callbacks.
+ if (mCallbackType == CALLBACK_LISTEN_ALL) {
+ recomputeLocalPrefixes();
+ }
+ }
+
+ @Override
+ public void onLost(Network network) {
+ if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
+ mDefaultInternetNetwork = null;
+ mIsDefaultCellularUpstream = false;
+ mEntitlementMgr.notifyUpstream(false);
+ Log.d(TAG, "Lost default Internet network: " + network);
+ notifyTarget(EVENT_DEFAULT_SWITCHED, null);
+ return;
+ }
+
+ handleLost(network);
+ // Any non-LISTEN_ALL callback will necessarily concern a network that will
+ // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
+ // So it's not useful to do this work for non-LISTEN_ALL callbacks.
+ if (mCallbackType == CALLBACK_LISTEN_ALL) {
+ recomputeLocalPrefixes();
+ }
+ }
+ }
+
+ private void releaseCallback(NetworkCallback cb) {
+ if (cb != null) cm().unregisterNetworkCallback(cb);
+ }
+
+ private void notifyTarget(int which, Network network) {
+ notifyTarget(which, mNetworkMap.get(network));
+ }
+
+ private void notifyTarget(int which, Object obj) {
+ mTarget.sendMessage(mWhat, which, 0, obj);
+ }
+
+ private static class TypeStatePair {
+ public int type = TYPE_NONE;
+ public UpstreamNetworkState ns = null;
+ }
+
+ private static TypeStatePair findFirstAvailableUpstreamByType(
+ Iterable<UpstreamNetworkState> netStates, Iterable<Integer> preferredTypes,
+ boolean isCellularUpstreamPermitted) {
+ final TypeStatePair result = new TypeStatePair();
+
+ for (int type : preferredTypes) {
+ NetworkCapabilities nc;
+ try {
+ nc = networkCapabilitiesForType(type);
+ } catch (IllegalArgumentException iae) {
+ Log.e(TAG, "No NetworkCapabilities mapping for legacy type: " + type);
+ continue;
+ }
+ if (!isCellularUpstreamPermitted && isCellular(nc)) {
+ continue;
+ }
+
+ for (UpstreamNetworkState value : netStates) {
+ if (!nc.satisfiedByNetworkCapabilities(value.networkCapabilities)) {
+ continue;
+ }
+
+ result.type = type;
+ result.ns = value;
+ return result;
+ }
+ }
+
+ return result;
+ }
+
+ private static HashSet<IpPrefix> allLocalPrefixes(Iterable<UpstreamNetworkState> netStates) {
+ final HashSet<IpPrefix> prefixSet = new HashSet<>();
+
+ for (UpstreamNetworkState ns : netStates) {
+ final LinkProperties lp = ns.linkProperties;
+ if (lp == null) continue;
+ prefixSet.addAll(PrefixUtils.localPrefixesFrom(lp));
+ }
+
+ return prefixSet;
+ }
+
+ /** Check whether upstream is cellular. */
+ static boolean isCellular(UpstreamNetworkState ns) {
+ return (ns != null) && isCellular(ns.networkCapabilities);
+ }
+
+ private static boolean isCellular(NetworkCapabilities nc) {
+ return (nc != null) && nc.hasTransport(TRANSPORT_CELLULAR)
+ && nc.hasCapability(NET_CAPABILITY_NOT_VPN);
+ }
+
+ private static boolean hasCapability(UpstreamNetworkState ns, int netCap) {
+ return (ns != null) && (ns.networkCapabilities != null)
+ && ns.networkCapabilities.hasCapability(netCap);
+ }
+
+ private static boolean isNetworkUsableAndNotCellular(UpstreamNetworkState ns) {
+ return (ns != null) && (ns.networkCapabilities != null) && (ns.linkProperties != null)
+ && !isCellular(ns.networkCapabilities);
+ }
+
+ private static UpstreamNetworkState findFirstDunNetwork(
+ Iterable<UpstreamNetworkState> netStates) {
+ for (UpstreamNetworkState ns : netStates) {
+ if (isCellular(ns) && hasCapability(ns, NET_CAPABILITY_DUN)) return ns;
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a legacy type (TYPE_WIFI, ...) returns the corresponding NetworkCapabilities instance.
+ * This function is used for deprecated legacy type and be disabled by default.
+ */
+ @VisibleForTesting
+ public static NetworkCapabilities networkCapabilitiesForType(int type) {
+ final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
+
+ // Map from type to transports.
+ final int notFound = -1;
+ final int transport = sLegacyTypeToTransport.get(type, notFound);
+ if (transport == notFound) {
+ throw new IllegalArgumentException("unknown legacy type: " + type);
+ }
+ builder.addTransportType(transport);
+
+ if (type == TYPE_MOBILE_DUN) {
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+ // DUN is restricted network, see NetworkCapabilities#FORCE_RESTRICTED_CAPABILITIES.
+ builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ } else {
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+ return builder.build();
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkState.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkState.java
new file mode 100644
index 0000000..bab9f84
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkState.java
@@ -0,0 +1,51 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Snapshot of tethering upstream network state.
+ */
+public class UpstreamNetworkState {
+ /** {@link LinkProperties}. */
+ public final LinkProperties linkProperties;
+ /** {@link NetworkCapabilities}. */
+ public final NetworkCapabilities networkCapabilities;
+ /** {@link Network}. */
+ public final Network network;
+
+ /** Constructs a new UpstreamNetworkState. */
+ public UpstreamNetworkState(LinkProperties linkProperties,
+ NetworkCapabilities networkCapabilities, Network network) {
+ this.linkProperties = linkProperties;
+ this.networkCapabilities = networkCapabilities;
+ this.network = network;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format("UpstreamNetworkState{%s, %s, %s}",
+ network == null ? "null" : network,
+ networkCapabilities == null ? "null" : networkCapabilities,
+ linkProperties == null ? "null" : linkProperties);
+ }
+}
diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp
new file mode 100644
index 0000000..8f31c57
--- /dev/null
+++ b/Tethering/tests/Android.bp
@@ -0,0 +1,28 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "TetheringTestsJarJarRules",
+ srcs: ["jarjar-rules.txt"],
+ visibility: [
+ "//frameworks/base/packages/Tethering/tests:__subpackages__",
+ "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+ ]
+}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
new file mode 100644
index 0000000..351b9f4
--- /dev/null
+++ b/Tethering/tests/integration/Android.bp
@@ -0,0 +1,110 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "TetheringIntegrationTestsDefaults",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ min_sdk_version: "30",
+ static_libs: [
+ "NetworkStackApiStableLib",
+ "androidx.test.rules",
+ "mockito-target-extended-minus-junit4",
+ "net-tests-utils",
+ "testables",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ jarjar_rules: ":NetworkStackJarJarRules",
+}
+
+android_library {
+ name: "TetheringIntegrationTestsLatestSdkLib",
+ target_sdk_version: "30",
+ platform_apis: true,
+ defaults: ["TetheringIntegrationTestsDefaults"],
+ visibility: [
+ "//packages/modules/Connectivity/tests/cts/tethering",
+ "//packages/modules/Connectivity/Tethering/tests/mts",
+ ]
+}
+
+android_library {
+ name: "TetheringIntegrationTestsLib",
+ target_sdk_version: "current",
+ platform_apis: true,
+ defaults: ["TetheringIntegrationTestsDefaults"],
+ visibility: [
+ "//packages/modules/Connectivity/tests/cts/tethering",
+ "//packages/modules/Connectivity/Tethering/tests/mts",
+ ]
+}
+
+android_test {
+ name: "TetheringIntegrationTests",
+ platform_apis: true,
+ defaults: ["TetheringIntegrationTestsDefaults"],
+ test_suites: [
+ "device-tests",
+ "mts",
+ ],
+ compile_multilib: "both",
+}
+
+// Special version of the tethering tests that includes all tests necessary for code coverage
+// purposes. This is currently the union of TetheringTests, TetheringIntegrationTests and
+// NetworkStackTests.
+android_test {
+ name: "TetheringCoverageTests",
+ platform_apis: true,
+ min_sdk_version: "30",
+ target_sdk_version: "30",
+ test_suites: ["device-tests", "mts"],
+ test_config: "AndroidTest_Coverage.xml",
+ defaults: ["libnetworkstackutilsjni_deps"],
+ static_libs: [
+ "modules-utils-native-coverage-listener",
+ "NetdStaticLibTestsLib",
+ "NetworkStaticLibTestsLib",
+ "NetworkStackTestsLib",
+ "TetheringTestsLatestSdkLib",
+ "TetheringIntegrationTestsLatestSdkLib",
+ ],
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ // For NetworkStackUtils included in NetworkStackBase
+ "libnetworkstackutilsjni",
+ "libtetherutilsjni",
+ ],
+ jarjar_rules: ":TetheringTestsJarJarRules",
+ compile_multilib: "both",
+ manifest: "AndroidManifest_coverage.xml",
+}
diff --git a/Tethering/tests/integration/AndroidManifest.xml b/Tethering/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..fddfaad
--- /dev/null
+++ b/Tethering/tests/integration/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.networkstack.tethering.tests.integration">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.networkstack.tethering.tests.integration"
+ android:label="Tethering integration tests">
+ </instrumentation>
+</manifest>
diff --git a/Tethering/tests/integration/AndroidManifest_coverage.xml b/Tethering/tests/integration/AndroidManifest_coverage.xml
new file mode 100644
index 0000000..06de00d
--- /dev/null
+++ b/Tethering/tests/integration/AndroidManifest_coverage.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.networkstack.tethering.tests.coverage">
+
+ <application tools:replace="android:label"
+ android:debuggable="true"
+ android:label="Tethering coverage tests">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.networkstack.tethering.tests.coverage"
+ android:label="Tethering coverage tests">
+ </instrumentation>
+</manifest>
diff --git a/Tethering/tests/integration/AndroidTest_Coverage.xml b/Tethering/tests/integration/AndroidTest_Coverage.xml
new file mode 100644
index 0000000..33c5b3d
--- /dev/null
+++ b/Tethering/tests/integration/AndroidTest_Coverage.xml
@@ -0,0 +1,13 @@
+<configuration description="Runs coverage tests for Tethering">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="TetheringCoverageTests.apk" />
+ </target_preparer>
+
+ <option name="test-tag" value="TetheringCoverageTests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.networkstack.tethering.tests.coverage" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ <option name="device-listeners" value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
+ </test>
+</configuration>
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
new file mode 100644
index 0000000..f1ddc6d
--- /dev/null
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -0,0 +1,706 @@
+/*
+ * 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 android.net;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.EthernetManager.TetheredInterfaceCallback;
+import android.net.EthernetManager.TetheredInterfaceRequest;
+import android.net.TetheringManager.StartTetheringCallback;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.dhcp.DhcpAckPacket;
+import android.net.dhcp.DhcpOfferPacket;
+import android.net.dhcp.DhcpPacket;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.Struct;
+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.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class EthernetTetheringTest {
+
+ private static final String TAG = EthernetTetheringTest.class.getSimpleName();
+ private static final int TIMEOUT_MS = 5000;
+ private static final int PACKET_READ_TIMEOUT_MS = 100;
+ private static final int DHCP_DISCOVER_ATTEMPTS = 10;
+ private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] {
+ DhcpPacket.DHCP_SUBNET_MASK,
+ DhcpPacket.DHCP_ROUTER,
+ DhcpPacket.DHCP_DNS_SERVER,
+ DhcpPacket.DHCP_LEASE_TIME,
+ };
+ private static final String DHCP_HOSTNAME = "testhostname";
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+ private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
+ private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
+
+ private TestNetworkInterface mTestIface;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TapPacketReader mTapPacketReader;
+
+ private TetheredInterfaceRequester mTetheredInterfaceRequester;
+ private MyTetheringEventCallback mTetheringEventCallback;
+
+ private UiAutomation mUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ private boolean mRunTests;
+
+ @Before
+ public void setUp() throws Exception {
+ // Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive
+ // tethered client callbacks.
+ mUiAutomation.adoptShellPermissionIdentity(
+ MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE);
+ mRunTests = mTm.isTetheringSupported() && mEm != null;
+ assumeTrue(mRunTests);
+
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+ }
+
+ private void cleanUp() throws Exception {
+ mTm.stopTethering(TETHERING_ETHERNET);
+ if (mTetheringEventCallback != null) {
+ mTetheringEventCallback.awaitInterfaceUntethered();
+ mTetheringEventCallback.unregister();
+ mTetheringEventCallback = null;
+ }
+ if (mTapPacketReader != null) {
+ TapPacketReader reader = mTapPacketReader;
+ mHandler.post(() -> reader.stop());
+ mTapPacketReader = null;
+ }
+ mHandlerThread.quitSafely();
+ mTetheredInterfaceRequester.release();
+ mEm.setIncludeTestInterfaces(false);
+ maybeDeleteTestInterface();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ try {
+ if (mRunTests) cleanUp();
+ } finally {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+ }
+
+ @Test
+ public void testVirtualEthernetAlreadyExists() throws Exception {
+ // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
+ assumeFalse(mEm.isAvailable());
+
+ mTestIface = createTestInterface();
+ // This must be done now because as soon as setIncludeTestInterfaces(true) is called, the
+ // interface will be placed in client mode, which will delete the link-local address.
+ // At that point NetworkInterface.getByName() will cease to work on the interface, because
+ // starting in R NetworkInterface can no longer see interfaces without IP addresses.
+ int mtu = getMTU(mTestIface);
+
+ Log.d(TAG, "Including test interfaces");
+ mEm.setIncludeTestInterfaces(true);
+
+ final String iface = mTetheredInterfaceRequester.getInterface();
+ assertEquals("TetheredInterfaceCallback for unexpected interface",
+ mTestIface.getInterfaceName(), iface);
+
+ checkVirtualEthernet(mTestIface, mtu);
+ }
+
+ @Test
+ public void testVirtualEthernet() throws Exception {
+ // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
+ assumeFalse(mEm.isAvailable());
+
+ CompletableFuture<String> futureIface = mTetheredInterfaceRequester.requestInterface();
+
+ mEm.setIncludeTestInterfaces(true);
+
+ mTestIface = createTestInterface();
+
+ final String iface = futureIface.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ assertEquals("TetheredInterfaceCallback for unexpected interface",
+ mTestIface.getInterfaceName(), iface);
+
+ checkVirtualEthernet(mTestIface, getMTU(mTestIface));
+ }
+
+ @Test
+ public void testStaticIpv4() throws Exception {
+ assumeFalse(mEm.isAvailable());
+
+ mEm.setIncludeTestInterfaces(true);
+
+ mTestIface = createTestInterface();
+
+ final String iface = mTetheredInterfaceRequester.getInterface();
+ assertEquals("TetheredInterfaceCallback for unexpected interface",
+ mTestIface.getInterfaceName(), iface);
+
+ assertInvalidStaticIpv4Request(iface, null, null);
+ assertInvalidStaticIpv4Request(iface, "2001:db8::1/64", "2001:db8:2::/64");
+ assertInvalidStaticIpv4Request(iface, "192.0.2.2/28", "2001:db8:2::/28");
+ assertInvalidStaticIpv4Request(iface, "2001:db8:2::/28", "192.0.2.2/28");
+ assertInvalidStaticIpv4Request(iface, "192.0.2.2/28", null);
+ assertInvalidStaticIpv4Request(iface, null, "192.0.2.2/28");
+ assertInvalidStaticIpv4Request(iface, "192.0.2.3/27", "192.0.2.2/28");
+
+ final String localAddr = "192.0.2.3/28";
+ final String clientAddr = "192.0.2.2/28";
+ mTetheringEventCallback = enableEthernetTethering(iface,
+ requestWithStaticIpv4(localAddr, clientAddr));
+
+ mTetheringEventCallback.awaitInterfaceTethered();
+ assertInterfaceHasIpAddress(iface, localAddr);
+
+ byte[] client1 = MacAddress.fromString("1:2:3:4:5:6").toByteArray();
+ byte[] client2 = MacAddress.fromString("a:b:c:d:e:f").toByteArray();
+
+ FileDescriptor fd = mTestIface.getFileDescriptor().getFileDescriptor();
+ mTapPacketReader = makePacketReader(fd, getMTU(mTestIface));
+ DhcpResults dhcpResults = runDhcp(fd, client1);
+ assertEquals(new LinkAddress(clientAddr), dhcpResults.ipAddress);
+
+ try {
+ runDhcp(fd, client2);
+ fail("Only one client should get an IP address");
+ } catch (TimeoutException expected) { }
+
+ }
+
+ private static boolean isRouterAdvertisement(byte[] pkt) {
+ if (pkt == null) return false;
+
+ ByteBuffer buf = ByteBuffer.wrap(pkt);
+
+ final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+ if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
+
+ final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+ if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
+
+ final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+ return icmpv6Hdr.type == (short) ICMPV6_ROUTER_ADVERTISEMENT;
+ }
+
+ private static void expectRouterAdvertisement(TapPacketReader reader, String iface,
+ long timeoutMs) {
+ final long deadline = SystemClock.uptimeMillis() + timeoutMs;
+ do {
+ byte[] pkt = reader.popPacket(timeoutMs);
+ if (isRouterAdvertisement(pkt)) return;
+ timeoutMs = deadline - SystemClock.uptimeMillis();
+ } while (timeoutMs > 0);
+ fail("Did not receive router advertisement on " + iface + " after "
+ + timeoutMs + "ms idle");
+ }
+
+ private static void expectLocalOnlyAddresses(String iface) throws Exception {
+ final List<InterfaceAddress> interfaceAddresses =
+ NetworkInterface.getByName(iface).getInterfaceAddresses();
+
+ boolean foundIpv6Ula = false;
+ for (InterfaceAddress ia : interfaceAddresses) {
+ final InetAddress addr = ia.getAddress();
+ if (isIPv6ULA(addr)) {
+ foundIpv6Ula = true;
+ }
+ final int prefixlen = ia.getNetworkPrefixLength();
+ final LinkAddress la = new LinkAddress(addr, prefixlen);
+ if (la.isIpv6() && la.isGlobalPreferred()) {
+ fail("Found global IPv6 address on local-only interface: " + interfaceAddresses);
+ }
+ }
+
+ assertTrue("Did not find IPv6 ULA on local-only interface " + iface,
+ foundIpv6Ula);
+ }
+
+ @Test
+ public void testLocalOnlyTethering() throws Exception {
+ assumeFalse(mEm.isAvailable());
+
+ mEm.setIncludeTestInterfaces(true);
+
+ mTestIface = createTestInterface();
+
+ final String iface = mTetheredInterfaceRequester.getInterface();
+ assertEquals("TetheredInterfaceCallback for unexpected interface",
+ mTestIface.getInterfaceName(), iface);
+
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
+ mTetheringEventCallback = enableEthernetTethering(iface, request);
+ mTetheringEventCallback.awaitInterfaceLocalOnly();
+
+ // makePacketReader only works after tethering is started, because until then the interface
+ // does not have an IP address, and unprivileged apps cannot see interfaces without IP
+ // addresses. This shouldn't be flaky because the TAP interface will buffer all packets even
+ // before the reader is started.
+ FileDescriptor fd = mTestIface.getFileDescriptor().getFileDescriptor();
+ mTapPacketReader = makePacketReader(fd, getMTU(mTestIface));
+
+ expectRouterAdvertisement(mTapPacketReader, iface, 2000 /* timeoutMs */);
+ expectLocalOnlyAddresses(iface);
+ }
+
+ private boolean isAdbOverNetwork() {
+ // If adb TCP port opened, this test may running by adb over network.
+ return (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1)
+ || (SystemProperties.getInt("service.adb.tcp.port", -1) > -1);
+ }
+
+ @Test
+ public void testPhysicalEthernet() throws Exception {
+ assumeTrue(mEm.isAvailable());
+ // Do not run this test if adb is over network and ethernet is connected.
+ // It is likely the adb run over ethernet, the adb would break when ethernet is switching
+ // from client mode to server mode. See b/160389275.
+ assumeFalse(isAdbOverNetwork());
+
+ // Get an interface to use.
+ final String iface = mTetheredInterfaceRequester.getInterface();
+
+ // Enable Ethernet tethering and check that it starts.
+ mTetheringEventCallback = enableEthernetTethering(iface);
+
+ // There is nothing more we can do on a physical interface without connecting an actual
+ // client, which is not possible in this test.
+ }
+
+ private static final class MyTetheringEventCallback implements TetheringEventCallback {
+ private final TetheringManager mTm;
+ private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
+ private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
+ private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
+ private final CountDownLatch mLocalOnlyStoppedLatch = new CountDownLatch(1);
+ private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1);
+ private final TetheringInterface mIface;
+
+ private volatile boolean mInterfaceWasTethered = false;
+ private volatile boolean mInterfaceWasLocalOnly = false;
+ private volatile boolean mUnregistered = false;
+ private volatile Collection<TetheredClient> mClients = null;
+
+ MyTetheringEventCallback(TetheringManager tm, String iface) {
+ mTm = tm;
+ mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ }
+
+ public void unregister() {
+ mTm.unregisterTetheringEventCallback(this);
+ mUnregistered = true;
+ }
+ @Override
+ public void onTetheredInterfacesChanged(List<String> interfaces) {
+ fail("Should only call callback that takes a Set<TetheringInterface>");
+ }
+
+ @Override
+ public void onTetheredInterfacesChanged(Set<TetheringInterface> interfaces) {
+ // Ignore stale callbacks registered by previous test cases.
+ if (mUnregistered) return;
+
+ if (!mInterfaceWasTethered && interfaces.contains(mIface)) {
+ // This interface is being tethered for the first time.
+ Log.d(TAG, "Tethering started: " + interfaces);
+ mInterfaceWasTethered = true;
+ mTetheringStartedLatch.countDown();
+ } else if (mInterfaceWasTethered && !interfaces.contains(mIface)) {
+ Log.d(TAG, "Tethering stopped: " + interfaces);
+ mTetheringStoppedLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
+ fail("Should only call callback that takes a Set<TetheringInterface>");
+ }
+
+ @Override
+ public void onLocalOnlyInterfacesChanged(Set<TetheringInterface> interfaces) {
+ // Ignore stale callbacks registered by previous test cases.
+ if (mUnregistered) return;
+
+ if (!mInterfaceWasLocalOnly && interfaces.contains(mIface)) {
+ // This interface is being put into local-only mode for the first time.
+ Log.d(TAG, "Local-only started: " + interfaces);
+ mInterfaceWasLocalOnly = true;
+ mLocalOnlyStartedLatch.countDown();
+ } else if (mInterfaceWasLocalOnly && !interfaces.contains(mIface)) {
+ Log.d(TAG, "Local-only stopped: " + interfaces);
+ mLocalOnlyStoppedLatch.countDown();
+ }
+ }
+
+ public void awaitInterfaceTethered() throws Exception {
+ assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms",
+ mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ public void awaitInterfaceLocalOnly() throws Exception {
+ assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms",
+ mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ public void awaitInterfaceUntethered() throws Exception {
+ // Don't block teardown if the interface was never tethered.
+ // This is racy because the interface might become tethered right after this check, but
+ // that can only happen in tearDown if startTethering timed out, which likely means
+ // the test has already failed.
+ if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return;
+
+ if (mInterfaceWasTethered) {
+ assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
+ mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } else if (mInterfaceWasLocalOnly) {
+ assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
+ mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } else {
+ fail(mIface + " cannot be both tethered and local-only. Update this test class.");
+ }
+ }
+
+ @Override
+ public void onError(String ifName, int error) {
+ // Ignore stale callbacks registered by previous test cases.
+ if (mUnregistered) return;
+
+ fail("TetheringEventCallback got error:" + error + " on iface " + ifName);
+ }
+
+ @Override
+ public void onClientsChanged(Collection<TetheredClient> clients) {
+ // Ignore stale callbacks registered by previous test cases.
+ if (mUnregistered) return;
+
+ Log.d(TAG, "Got clients changed: " + clients);
+ mClients = clients;
+ if (clients.size() > 0) {
+ mClientConnectedLatch.countDown();
+ }
+ }
+
+ public Collection<TetheredClient> awaitClientConnected() throws Exception {
+ assertTrue("Did not receive client connected callback after " + TIMEOUT_MS + "ms",
+ mClientConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ return mClients;
+ }
+ }
+
+ private MyTetheringEventCallback enableEthernetTethering(String iface,
+ TetheringRequest request) throws Exception {
+ MyTetheringEventCallback callback = new MyTetheringEventCallback(mTm, iface);
+ mTm.registerTetheringEventCallback(mHandler::post, callback);
+
+ StartTetheringCallback startTetheringCallback = new StartTetheringCallback() {
+ @Override
+ public void onTetheringFailed(int resultCode) {
+ fail("Unexpectedly got onTetheringFailed");
+ }
+ };
+ Log.d(TAG, "Starting Ethernet tethering");
+ mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+
+ final int connectivityType = request.getConnectivityScope();
+ switch (connectivityType) {
+ case CONNECTIVITY_SCOPE_GLOBAL:
+ callback.awaitInterfaceTethered();
+ break;
+ case CONNECTIVITY_SCOPE_LOCAL:
+ callback.awaitInterfaceLocalOnly();
+ break;
+ default:
+ fail("Unexpected connectivity type requested: " + connectivityType);
+ }
+
+ return callback;
+ }
+
+ private MyTetheringEventCallback enableEthernetTethering(String iface) throws Exception {
+ return enableEthernetTethering(iface,
+ new TetheringRequest.Builder(TETHERING_ETHERNET)
+ .setShouldShowEntitlementUi(false).build());
+ }
+
+ private int getMTU(TestNetworkInterface iface) throws SocketException {
+ NetworkInterface nif = NetworkInterface.getByName(iface.getInterfaceName());
+ assertNotNull("Can't get NetworkInterface object for " + iface.getInterfaceName(), nif);
+ return nif.getMTU();
+ }
+
+ private TapPacketReader makePacketReader(FileDescriptor fd, int mtu) {
+ final TapPacketReader reader = new TapPacketReader(mHandler, fd, mtu);
+ mHandler.post(() -> reader.start());
+ HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
+ return reader;
+ }
+
+ private void checkVirtualEthernet(TestNetworkInterface iface, int mtu) throws Exception {
+ FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor();
+ mTapPacketReader = makePacketReader(fd, mtu);
+ mTetheringEventCallback = enableEthernetTethering(iface.getInterfaceName());
+ checkTetheredClientCallbacks(fd);
+ }
+
+ private DhcpResults runDhcp(FileDescriptor fd, byte[] clientMacAddr) throws Exception {
+ // We have to retransmit DHCP requests because IpServer declares itself to be ready before
+ // its DhcpServer is actually started. TODO: fix this race and remove this loop.
+ DhcpPacket offerPacket = null;
+ for (int i = 0; i < DHCP_DISCOVER_ATTEMPTS; i++) {
+ Log.d(TAG, "Sending DHCP discover");
+ sendDhcpDiscover(fd, clientMacAddr);
+ offerPacket = getNextDhcpPacket();
+ if (offerPacket instanceof DhcpOfferPacket) break;
+ }
+ if (!(offerPacket instanceof DhcpOfferPacket)) {
+ throw new TimeoutException("No DHCPOFFER received on interface within timeout");
+ }
+
+ sendDhcpRequest(fd, offerPacket, clientMacAddr);
+ DhcpPacket ackPacket = getNextDhcpPacket();
+ if (!(ackPacket instanceof DhcpAckPacket)) {
+ throw new TimeoutException("No DHCPACK received on interface within timeout");
+ }
+
+ return ackPacket.toDhcpResults();
+ }
+
+ private void checkTetheredClientCallbacks(FileDescriptor fd) throws Exception {
+ // Create a fake client.
+ byte[] clientMacAddr = new byte[6];
+ new Random().nextBytes(clientMacAddr);
+
+ DhcpResults dhcpResults = runDhcp(fd, clientMacAddr);
+
+ final Collection<TetheredClient> clients = mTetheringEventCallback.awaitClientConnected();
+ assertEquals(1, clients.size());
+ final TetheredClient client = clients.iterator().next();
+
+ // Check the MAC address.
+ assertEquals(MacAddress.fromBytes(clientMacAddr), client.getMacAddress());
+ assertEquals(TETHERING_ETHERNET, client.getTetheringType());
+
+ // Check the hostname.
+ assertEquals(1, client.getAddresses().size());
+ TetheredClient.AddressInfo info = client.getAddresses().get(0);
+ assertEquals(DHCP_HOSTNAME, info.getHostname());
+
+ // Check the address is the one that was handed out in the DHCP ACK.
+ assertLinkAddressMatches(dhcpResults.ipAddress, info.getAddress());
+
+ // Check that the lifetime is correct +/- 10s.
+ final long now = SystemClock.elapsedRealtime();
+ final long actualLeaseDuration = (info.getAddress().getExpirationTime() - now) / 1000;
+ final String msg = String.format("IP address should have lifetime of %d, got %d",
+ dhcpResults.leaseDuration, actualLeaseDuration);
+ assertTrue(msg, Math.abs(dhcpResults.leaseDuration - actualLeaseDuration) < 10);
+ }
+
+ private DhcpPacket getNextDhcpPacket() throws ParseException {
+ byte[] packet;
+ while ((packet = mTapPacketReader.popPacket(PACKET_READ_TIMEOUT_MS)) != null) {
+ try {
+ return DhcpPacket.decodeFullPacket(packet, packet.length, DhcpPacket.ENCAP_L2);
+ } catch (DhcpPacket.ParseException e) {
+ // Not a DHCP packet. Continue.
+ }
+ }
+ return null;
+ }
+
+ private static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
+ private final Handler mHandler;
+ private final EthernetManager mEm;
+
+ private TetheredInterfaceRequest mRequest;
+ private final CompletableFuture<String> mFuture = new CompletableFuture<>();
+
+ TetheredInterfaceRequester(Handler handler, EthernetManager em) {
+ mHandler = handler;
+ mEm = em;
+ }
+
+ @Override
+ public void onAvailable(String iface) {
+ Log.d(TAG, "Ethernet interface available: " + iface);
+ mFuture.complete(iface);
+ }
+
+ @Override
+ public void onUnavailable() {
+ 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 = mEm.requestTetheredInterface(mHandler::post, this);
+ return mFuture;
+ }
+
+ public String getInterface() throws Exception {
+ return requestInterface().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ public void release() {
+ if (mRequest != null) {
+ mFuture.obtrudeException(new IllegalStateException("Request already released"));
+ mRequest.release();
+ mRequest = null;
+ }
+ }
+ }
+
+ private void sendDhcpDiscover(FileDescriptor fd, byte[] macAddress) throws Exception {
+ ByteBuffer packet = DhcpPacket.buildDiscoverPacket(DhcpPacket.ENCAP_L2,
+ new Random().nextInt() /* transactionId */, (short) 0 /* secs */,
+ macAddress, false /* unicast */, DHCP_REQUESTED_PARAMS,
+ false /* rapid commit */, DHCP_HOSTNAME);
+ sendPacket(fd, packet);
+ }
+
+ private void sendDhcpRequest(FileDescriptor fd, DhcpPacket offerPacket, byte[] macAddress)
+ throws Exception {
+ DhcpResults results = offerPacket.toDhcpResults();
+ Inet4Address clientIp = (Inet4Address) results.ipAddress.getAddress();
+ Inet4Address serverIdentifier = results.serverAddress;
+ ByteBuffer packet = DhcpPacket.buildRequestPacket(DhcpPacket.ENCAP_L2,
+ 0 /* transactionId */, (short) 0 /* secs */, DhcpPacket.INADDR_ANY /* clientIp */,
+ false /* broadcast */, macAddress, clientIp /* requestedIpAddress */,
+ serverIdentifier, DHCP_REQUESTED_PARAMS, DHCP_HOSTNAME);
+ sendPacket(fd, packet);
+ }
+
+ private void sendPacket(FileDescriptor fd, ByteBuffer packet) throws Exception {
+ assertNotNull("Only tests on virtual interfaces can send packets", fd);
+ Os.write(fd, packet);
+ }
+
+ public void assertLinkAddressMatches(LinkAddress l1, LinkAddress l2) {
+ // Check all fields except the deprecation and expiry times.
+ String msg = String.format("LinkAddresses do not match. expected: %s actual: %s", l1, l2);
+ assertTrue(msg, l1.isSameAddressAs(l2));
+ assertEquals("LinkAddress flags do not match", l1.getFlags(), l2.getFlags());
+ assertEquals("LinkAddress scope does not match", l1.getScope(), l2.getScope());
+ }
+
+ private TetheringRequest requestWithStaticIpv4(String local, String client) {
+ LinkAddress localAddr = local == null ? null : new LinkAddress(local);
+ LinkAddress clientAddr = client == null ? null : new LinkAddress(client);
+ return new TetheringRequest.Builder(TETHERING_ETHERNET)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setShouldShowEntitlementUi(false).build();
+ }
+
+ private void assertInvalidStaticIpv4Request(String iface, String local, String client)
+ throws Exception {
+ try {
+ enableEthernetTethering(iface, requestWithStaticIpv4(local, client));
+ fail("Unexpectedly accepted invalid IPv4 configuration: " + local + ", " + client);
+ } catch (IllegalArgumentException | NullPointerException expected) { }
+ }
+
+ private void assertInterfaceHasIpAddress(String iface, String expected) throws Exception {
+ LinkAddress expectedAddr = new LinkAddress(expected);
+ NetworkInterface nif = NetworkInterface.getByName(iface);
+ for (InterfaceAddress ia : nif.getInterfaceAddresses()) {
+ final LinkAddress addr = new LinkAddress(ia.getAddress(), ia.getNetworkPrefixLength());
+ if (expectedAddr.equals(addr)) {
+ return;
+ }
+ }
+ fail("Expected " + iface + " to have IP address " + expected + ", found "
+ + nif.getInterfaceAddresses());
+ }
+
+ private TestNetworkInterface createTestInterface() throws Exception {
+ TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
+ TestNetworkInterface iface = tnm.createTapInterface();
+ Log.d(TAG, "Created test interface " + iface.getInterfaceName());
+ return iface;
+ }
+
+ private void maybeDeleteTestInterface() throws Exception {
+ if (mTestIface != null) {
+ mTestIface.getFileDescriptor().close();
+ Log.d(TAG, "Deleted test interface " + mTestIface.getInterfaceName());
+ mTestIface = null;
+ }
+ }
+}
diff --git a/Tethering/tests/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt
new file mode 100644
index 0000000..9cb143e
--- /dev/null
+++ b/Tethering/tests/jarjar-rules.txt
@@ -0,0 +1,19 @@
+# Don't jar-jar the entire package because this test use some
+# internal classes (like ArrayUtils in com.android.internal.util)
+rule com.android.internal.util.BitUtils* com.android.networkstack.tethering.util.BitUtils@1
+rule com.android.internal.util.IndentingPrintWriter* com.android.networkstack.tethering.util.IndentingPrintWriter@1
+rule com.android.internal.util.IState* com.android.networkstack.tethering.util.IState@1
+rule com.android.internal.util.MessageUtils* com.android.networkstack.tethering.util.MessageUtils@1
+rule com.android.internal.util.State* com.android.networkstack.tethering.util.State@1
+rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1
+rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1
+
+rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
+
+# Classes from net-utils-framework-common
+rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1
+
+# TODO: either stop using frameworks-base-testutils or remove the unit test classes it contains.
+# TestableLooper from "testables" can be used instead of TestLooper from frameworks-base-testutils.
+zap android.os.test.TestLooperTest*
+zap com.android.test.filters.SelectTestTests*
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
new file mode 100644
index 0000000..e51d531
--- /dev/null
+++ b/Tethering/tests/mts/Android.bp
@@ -0,0 +1,65 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ // This tests for functionality that is not required for devices that
+ // don't use Tethering mainline module.
+ name: "MtsTetheringTestLatestSdk",
+
+ min_sdk_version: "30",
+ target_sdk_version: "30",
+
+ libs: [
+ "android.test.base",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ static_libs: [
+ "androidx.test.rules",
+ // mockito-target-extended-minus-junit4 used in this lib have dependency with
+ // jni_libs libdexmakerjvmtiagent and libstaticjvmtiagent.
+ "cts-net-utils",
+ // This is needed for androidx.test.runner.AndroidJUnitRunner.
+ "ctstestrunner-axt",
+ "junit",
+ "junit-params",
+ ],
+
+ jni_libs: [
+ // For mockito extended which is pulled in from -net-utils -> net-tests-utils
+ // (mockito-target-extended-minus-junit4).
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+
+ defaults: ["framework-connectivity-test-defaults"],
+
+ platform_apis: true,
+
+ // Tag this module as a mts test artifact
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+}
diff --git a/Tethering/tests/mts/AndroidManifest.xml b/Tethering/tests/mts/AndroidManifest.xml
new file mode 100644
index 0000000..6d2abca
--- /dev/null
+++ b/Tethering/tests/mts/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.tethering.mts">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.tethering.mts"
+ android:label="MTS tests of android.tethering">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener" />
+ </instrumentation>
+
+</manifest>
diff --git a/Tethering/tests/mts/AndroidTest.xml b/Tethering/tests/mts/AndroidTest.xml
new file mode 100644
index 0000000..4edd544
--- /dev/null
+++ b/Tethering/tests/mts/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for MTS Tethering test cases">
+ <option name="test-suite-tag" value="mts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <!-- Instant app do not have INTERNET permission. -->
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <!-- Feature is not backed by native code. -->
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <!-- Allow running this against a secondary user. -->
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="MtsTetheringTestLatestSdk.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.tethering.mts" />
+ </test>
+
+ <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/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
new file mode 100644
index 0000000..e0fcbfa
--- /dev/null
+++ b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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 android.tethering.mts;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.Manifest.permission.WRITE_SETTINGS;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.TetheringInterface;
+import android.net.TetheringManager;
+import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TestNetworkTracker;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class TetheringModuleTest {
+ private Context mContext;
+ private TetheringManager mTm;
+ private CtsTetheringUtils mCtsTetheringUtils;
+
+ private UiAutomation mUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+ @Before
+ public void setUp() throws Exception {
+ mUiAutomation.adoptShellPermissionIdentity(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS,
+ WRITE_SETTINGS, READ_DEVICE_CONFIG, TETHER_PRIVILEGED);
+ mContext = InstrumentationRegistry.getContext();
+ mTm = mContext.getSystemService(TetheringManager.class);
+ mCtsTetheringUtils = new CtsTetheringUtils(mContext);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ private static final String TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES =
+ "tether_enable_select_all_prefix_ranges";
+ @Test
+ public void testSwitchBasePrefixRangeWhenConflict() throws Exception {
+ assumeTrue(isFeatureEnabled(TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES, true));
+
+ addressConflictTest(true);
+ }
+
+ @Test
+ public void testSwitchPrefixRangeWhenConflict() throws Exception {
+ addressConflictTest(false);
+ }
+
+ private void addressConflictTest(final boolean wholeRangeConflict) throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+
+ TestNetworkTracker tnt = null;
+ try {
+ tetherEventCallback.assumeTetheringSupported();
+ assumeTrue(isWifiTetheringSupported(tetherEventCallback));
+ tetherEventCallback.expectNoTetheringActive();
+
+ final TetheringInterface tetheredIface =
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ assertNotNull(tetheredIface);
+ final String wifiTetheringIface = tetheredIface.getInterface();
+
+ NetworkInterface nif = NetworkInterface.getByName(wifiTetheringIface);
+ // Tethering downstream only have one ipv4 address.
+ final LinkAddress hotspotAddr = getFirstIpv4Address(nif);
+ assertNotNull(hotspotAddr);
+
+ final IpPrefix testPrefix = getConflictingPrefix(hotspotAddr, wholeRangeConflict);
+ assertNotNull(testPrefix);
+
+ tnt = setUpTestNetwork(
+ new LinkAddress(testPrefix.getAddress(), testPrefix.getPrefixLength()));
+
+ tetherEventCallback.expectNoTetheringActive();
+ final List<String> wifiRegexs =
+ tetherEventCallback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
+
+ tetherEventCallback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI);
+ nif = NetworkInterface.getByName(wifiTetheringIface);
+ final LinkAddress newHotspotAddr = getFirstIpv4Address(nif);
+ assertNotNull(newHotspotAddr);
+
+ assertFalse(testPrefix.containsPrefix(
+ new IpPrefix(newHotspotAddr.getAddress(), newHotspotAddr.getPrefixLength())));
+
+ mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
+ } finally {
+ if (tnt != null) {
+ tnt.teardown();
+ }
+ mTm.stopAllTethering();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+ }
+
+ private LinkAddress getFirstIpv4Address(final NetworkInterface nif) {
+ for (InterfaceAddress ia : nif.getInterfaceAddresses()) {
+ final LinkAddress addr = new LinkAddress(ia.getAddress(), ia.getNetworkPrefixLength());
+ if (addr.isIpv4()) return addr;
+ }
+ return null;
+ }
+
+ @NonNull
+ private IpPrefix getConflictingPrefix(final LinkAddress address,
+ final boolean wholeRangeConflict) {
+ if (!wholeRangeConflict) {
+ return new IpPrefix(address.getAddress(), address.getPrefixLength());
+ }
+
+ final ArrayList<IpPrefix> prefixPool = new ArrayList<>(Arrays.asList(
+ new IpPrefix("192.168.0.0/16"),
+ new IpPrefix("172.16.0.0/12"),
+ new IpPrefix("10.0.0.0/8")));
+
+ for (IpPrefix prefix : prefixPool) {
+ if (prefix.contains(address.getAddress())) return prefix;
+ }
+
+ fail("Could not find sutiable conflict prefix");
+
+ // Never go here.
+ return null;
+ }
+
+ private TestNetworkTracker setUpTestNetwork(final LinkAddress address) throws Exception {
+ return initTestNetwork(mContext, address, 10_000L /* test timeout ms*/);
+
+ }
+
+ public static boolean isFeatureEnabled(final String name, final boolean defaultValue) {
+ return DeviceConfig.getBoolean(NAMESPACE_CONNECTIVITY, name, defaultValue);
+ }
+}
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
new file mode 100644
index 0000000..75fdd6e
--- /dev/null
+++ b/Tethering/tests/privileged/Android.bp
@@ -0,0 +1,53 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "TetheringPrivilegedTestsJniDefaults",
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ "libtetherutilsjni",
+ ],
+ jni_uses_sdk_apis: true,
+ visibility: ["//visibility:private"],
+}
+
+android_test {
+ name: "TetheringPrivilegedTests",
+ defaults: [
+ "TetheringPrivilegedTestsJniDefaults",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ certificate: "networkstack",
+ platform_apis: true,
+ test_suites: [
+ "device-tests",
+ "mts",
+ ],
+ static_libs: [
+ "androidx.test.rules",
+ "net-tests-utils",
+ "TetheringApiCurrentLib",
+ ],
+ compile_multilib: "both",
+}
diff --git a/Tethering/tests/privileged/AndroidManifest.xml b/Tethering/tests/privileged/AndroidManifest.xml
new file mode 100644
index 0000000..49eba15
--- /dev/null
+++ b/Tethering/tests/privileged/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.networkstack.tethering.tests.privileged"
+ android:sharedUserId="android.uid.networkstack">
+
+ <!-- Note: do not add any privileged or signature permissions that are granted
+ to the network stack and its shared uid apps. Otherwise, the test APK will
+ install, but when the device is rebooted, it will bootloop because this
+ test APK is not in the privileged permission allow list -->
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.networkstack.tethering.tests.privileged"
+ android:label="Tethering privileged tests">
+ </instrumentation>
+</manifest>
diff --git a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
new file mode 100644
index 0000000..a933e1b
--- /dev/null
+++ b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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 android.net.ip;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.IpUtils.icmpv6Checksum;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.MacAddress;
+import android.net.util.InterfaceParams;
+import android.net.util.TetheringUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TapPacketReaderRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DadProxyTest {
+ private static final int DATA_BUFFER_LEN = 4096;
+ private static final int PACKET_TIMEOUT_MS = 2_000; // Long enough for DAD to succeed.
+
+ // Start the readers manually on a common handler shared with DadProxy, for simplicity
+ @Rule
+ public final TapPacketReaderRule mUpstreamReader = new TapPacketReaderRule(
+ DATA_BUFFER_LEN, false /* autoStart */);
+ @Rule
+ public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule(
+ DATA_BUFFER_LEN, false /* autoStart */);
+
+ private InterfaceParams mUpstreamParams, mTetheredParams;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TapPacketReader mUpstreamPacketReader, mTetheredPacketReader;
+
+ private static INetd sNetd;
+
+ @BeforeClass
+ public static void setupOnce() {
+ System.loadLibrary("tetherutilsjni");
+
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ final IBinder netdIBinder =
+ (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
+ sNetd = INetd.Stub.asInterface(netdIBinder);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ setupTapInterfaces();
+
+ // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+ if (Looper.myLooper() == null) Looper.prepare();
+
+ DadProxy mProxy = setupProxy();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mUpstreamReader.stop();
+ mTetheredReader.stop();
+
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join(PACKET_TIMEOUT_MS);
+ }
+
+ if (mTetheredParams != null) {
+ sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+ }
+ if (mUpstreamParams != null) {
+ sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name);
+ }
+ }
+
+ private void setupTapInterfaces() throws Exception {
+ // Create upstream test iface.
+ mUpstreamReader.start(mHandler);
+ final String upstreamIface = mUpstreamReader.iface.getInterfaceName();
+ mUpstreamParams = InterfaceParams.getByName(upstreamIface);
+ assertNotNull(mUpstreamParams);
+ mUpstreamPacketReader = mUpstreamReader.getReader();
+
+ // Create tethered test iface.
+ mTetheredReader.start(mHandler);
+ final String tetheredIface = mTetheredReader.getIface().getInterfaceName();
+ mTetheredParams = InterfaceParams.getByName(tetheredIface);
+ assertNotNull(mTetheredParams);
+ mTetheredPacketReader = mTetheredReader.getReader();
+ }
+
+ private static final int IPV6_HEADER_LEN = 40;
+ private static final int ETH_HEADER_LEN = 14;
+ private static final int ICMPV6_NA_NS_LEN = 24;
+ private static final int LL_TARGET_OPTION_LEN = 8;
+ private static final int ICMPV6_CHECKSUM_OFFSET = 2;
+ private static final int ETHER_TYPE_IPV6 = 0x86dd;
+
+ private static ByteBuffer createDadPacket(int type) {
+ // Refer to buildArpPacket()
+ int icmpLen = ICMPV6_NA_NS_LEN
+ + (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT
+ ? LL_TARGET_OPTION_LEN : 0);
+ final ByteBuffer buf = ByteBuffer.allocate(icmpLen + IPV6_HEADER_LEN + ETH_HEADER_LEN);
+
+ // Ethernet header.
+ final MacAddress srcMac = MacAddress.fromString("33:33:ff:66:77:88");
+ buf.put(srcMac.toByteArray());
+ final MacAddress dstMac = MacAddress.fromString("01:02:03:04:05:06");
+ buf.put(dstMac.toByteArray());
+ buf.putShort((short) ETHER_TYPE_IPV6);
+
+ // IPv6 header
+ byte[] version = {(byte) 0x60, 0x00, 0x00, 0x00};
+ buf.put(version); // Version
+ buf.putShort((byte) icmpLen); // Length
+ buf.put((byte) IPPROTO_ICMPV6); // Next header
+ buf.put((byte) 0xff); // Hop limit
+
+ final byte[] target =
+ InetAddresses.parseNumericAddress("fe80::1122:3344:5566:7788").getAddress();
+ final byte[] src;
+ final byte[] dst;
+ if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION) {
+ src = InetAddresses.parseNumericAddress("::").getAddress();
+ dst = InetAddresses.parseNumericAddress("ff02::1:ff66:7788").getAddress();
+ } else {
+ src = target;
+ dst = TetheringUtils.ALL_NODES;
+ }
+ buf.put(src);
+ buf.put(dst);
+
+ // ICMPv6 Header
+ buf.put((byte) type); // Type
+ buf.put((byte) 0x00); // Code
+ buf.putShort((short) 0); // Checksum
+ buf.putInt(0); // Reserved
+ buf.put(target);
+
+ if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+ //NA packet has LL target address
+ //ICMPv6 Option
+ buf.put((byte) 0x02); // Type
+ buf.put((byte) 0x01); // Length
+ byte[] ll_target = MacAddress.fromString("01:02:03:04:05:06").toByteArray();
+ buf.put(ll_target);
+ }
+
+ // Populate checksum field
+ final int transportOffset = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+ final short checksum = icmpv6Checksum(buf, ETH_HEADER_LEN, transportOffset, icmpLen);
+ buf.putShort(transportOffset + ICMPV6_CHECKSUM_OFFSET, checksum);
+
+ buf.flip();
+ return buf;
+ }
+
+ private DadProxy setupProxy() throws Exception {
+ DadProxy proxy = new DadProxy(mHandler, mTetheredParams);
+ mHandler.post(() -> proxy.setUpstreamIface(mUpstreamParams));
+
+ // Upstream iface is added to local network to simplify test case.
+ // Otherwise the test needs to create and destroy a network for the upstream iface.
+ sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name);
+ sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+
+ return proxy;
+ }
+
+ // TODO: change to assert.
+ private boolean waitForPacket(ByteBuffer packet, TapPacketReader reader) {
+ byte[] p;
+
+ while ((p = reader.popPacket(PACKET_TIMEOUT_MS)) != null) {
+ final ByteBuffer buffer = ByteBuffer.wrap(p);
+
+ if (buffer.compareTo(packet) == 0) return true;
+ }
+ return false;
+ }
+
+ private ByteBuffer copy(ByteBuffer buf) {
+ // There does not seem to be a way to copy ByteBuffers. ByteBuffer does not implement
+ // clone() and duplicate() copies the metadata but shares the contents.
+ return ByteBuffer.wrap(buf.array().clone());
+ }
+
+ private void updateDstMac(ByteBuffer buf, MacAddress mac) {
+ buf.put(mac.toByteArray());
+ buf.rewind();
+ }
+ private void updateSrcMac(ByteBuffer buf, InterfaceParams ifaceParams) {
+ buf.position(ETHER_SRC_ADDR_OFFSET);
+ buf.put(ifaceParams.macAddr.toByteArray());
+ buf.rewind();
+ }
+
+ private void receivePacketAndMaybeExpectForwarded(boolean expectForwarded,
+ ByteBuffer in, TapPacketReader inReader, ByteBuffer out, TapPacketReader outReader)
+ throws IOException {
+
+ inReader.sendResponse(in);
+ if (waitForPacket(out, outReader)) return;
+
+ // When the test runs, DAD may be in progress, because the interface has just been created.
+ // If so, the DAD proxy will get EADDRNOTAVAIL when trying to send packets. It is not
+ // possible to work around this using IPV6_FREEBIND or IPV6_TRANSPARENT options because the
+ // kernel rawv6 code doesn't consider those options either when binding or when sending, and
+ // doesn't get the source address from the packet even in IPPROTO_RAW/HDRINCL mode (it only
+ // gets it from the socket or from cmsg).
+ //
+ // If DAD was in progress when the above was attempted, try again and expect the packet to
+ // be forwarded. Don't disable DAD in the test because if we did, the test would not notice
+ // if, for example, the DAD proxy code just crashed if it received EADDRNOTAVAIL.
+ final String msg = expectForwarded
+ ? "Did not receive expected packet even after waiting for DAD:"
+ : "Unexpectedly received packet:";
+
+ inReader.sendResponse(in);
+ assertEquals(msg, expectForwarded, waitForPacket(out, outReader));
+ }
+
+ private void receivePacketAndExpectForwarded(ByteBuffer in, TapPacketReader inReader,
+ ByteBuffer out, TapPacketReader outReader) throws IOException {
+ receivePacketAndMaybeExpectForwarded(true, in, inReader, out, outReader);
+ }
+
+ private void receivePacketAndExpectNotForwarded(ByteBuffer in, TapPacketReader inReader,
+ ByteBuffer out, TapPacketReader outReader) throws IOException {
+ receivePacketAndMaybeExpectForwarded(false, in, inReader, out, outReader);
+ }
+
+ @Test
+ public void testNaForwardingFromUpstreamToTether() throws Exception {
+ ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
+
+ ByteBuffer out = copy(na);
+ updateDstMac(out, MacAddress.fromString("33:33:00:00:00:01"));
+ updateSrcMac(out, mTetheredParams);
+
+ receivePacketAndExpectForwarded(na, mUpstreamPacketReader, out, mTetheredPacketReader);
+ }
+
+ @Test
+ // TODO: remove test once DAD works in both directions.
+ public void testNaForwardingFromTetherToUpstream() throws Exception {
+ ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
+
+ ByteBuffer out = copy(na);
+ updateDstMac(out, MacAddress.fromString("33:33:00:00:00:01"));
+ updateSrcMac(out, mTetheredParams);
+
+ receivePacketAndExpectNotForwarded(na, mTetheredPacketReader, out, mUpstreamPacketReader);
+ }
+
+ @Test
+ public void testNsForwardingFromTetherToUpstream() throws Exception {
+ ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
+
+ ByteBuffer out = copy(ns);
+ updateSrcMac(out, mUpstreamParams);
+
+ receivePacketAndExpectForwarded(ns, mTetheredPacketReader, out, mUpstreamPacketReader);
+ }
+
+ @Test
+ // TODO: remove test once DAD works in both directions.
+ public void testNsForwardingFromUpstreamToTether() throws Exception {
+ ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
+
+ ByteBuffer out = copy(ns);
+ updateSrcMac(ns, mUpstreamParams);
+
+ receivePacketAndExpectNotForwarded(ns, mUpstreamPacketReader, out, mTetheredPacketReader);
+ }
+}
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
new file mode 100644
index 0000000..1d94214
--- /dev/null
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.RouteInfo.RTN_UNICAST;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.net.RouteInfo;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.shared.RouteUtils;
+import android.net.util.InterfaceParams;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+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.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.MtuOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.RdnssOption;
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TapPacketReaderRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class RouterAdvertisementDaemonTest {
+ private static final String TAG = RouterAdvertisementDaemonTest.class.getSimpleName();
+ private static final int DATA_BUFFER_LEN = 4096;
+ private static final int PACKET_TIMEOUT_MS = 5_000;
+
+ @Rule
+ public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule(
+ DATA_BUFFER_LEN, false /* autoStart */);
+
+ private InterfaceParams mTetheredParams;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TapPacketReader mTetheredPacketReader;
+ private RouterAdvertisementDaemon mRaDaemon;
+
+ private static INetd sNetd;
+
+ @BeforeClass
+ public static void setupOnce() {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ final IBinder netdIBinder =
+ (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
+ sNetd = INetd.Stub.asInterface(netdIBinder);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ setupTapInterfaces();
+
+ // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+ if (Looper.myLooper() == null) Looper.prepare();
+
+ mRaDaemon = new RouterAdvertisementDaemon(mTetheredParams);
+ sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTetheredReader.stop();
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join(PACKET_TIMEOUT_MS);
+ }
+
+ if (mTetheredParams != null) {
+ sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+ }
+ }
+
+ private void setupTapInterfaces() {
+ // Create tethered test iface.
+ mTetheredReader.start(mHandler);
+ mTetheredParams = InterfaceParams.getByName(mTetheredReader.iface.getInterfaceName());
+ assertNotNull(mTetheredParams);
+ mTetheredPacketReader = mTetheredReader.getReader();
+ mHandler.post(mTetheredPacketReader::start);
+ }
+
+ private class TestRaPacket {
+ final RaParams mNewParams, mOldParams;
+
+ TestRaPacket(final RaParams oldParams, final RaParams newParams) {
+ mOldParams = oldParams;
+ mNewParams = newParams;
+ }
+
+ public boolean isPacketMatched(final byte[] pkt, boolean multicast) throws Exception {
+ if (pkt.length < (ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_RA_HEADER_LEN)) {
+ return false;
+ }
+ final ByteBuffer buf = ByteBuffer.wrap(pkt);
+
+ // Parse Ethernet header
+ final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+ if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
+
+ // Parse IPv6 header
+ final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+ assertEquals((ipv6Hdr.vtf >> 28), 6 /* ip version*/);
+
+ final int payLoadLength = pkt.length - ETHER_HEADER_LEN - IPV6_HEADER_LEN;
+ assertEquals(payLoadLength, ipv6Hdr.payloadLength);
+
+ // Parse ICMPv6 header
+ final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Hdr.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) return false;
+
+ // Check whether IPv6 destination address is multicast or unicast
+ if (multicast) {
+ assertEquals(ipv6Hdr.dstIp, IPV6_ADDR_ALL_NODES_MULTICAST);
+ } else {
+ // The unicast IPv6 destination address in RA can be either link-local or global
+ // IPv6 address. This test only expects link-local address.
+ assertTrue(ipv6Hdr.dstIp.isLinkLocalAddress());
+ }
+
+ // Parse RA header
+ final RaHeader raHdr = Struct.parse(RaHeader.class, buf);
+ assertEquals(mNewParams.hopLimit, raHdr.hopLimit);
+
+ while (buf.position() < pkt.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ switch (type) {
+ case ICMPV6_ND_OPTION_PIO:
+ // length is 4 because this test only expects one PIO included in the
+ // router advertisement packet.
+ assertEquals(4, length);
+
+ final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ assertEquals((byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), pio.flags);
+
+ final InetAddress address = InetAddress.getByAddress(pio.prefix);
+ final IpPrefix prefix = new IpPrefix(address, pio.prefixLen);
+ if (mNewParams.prefixes.contains(prefix)) {
+ assertTrue(pio.validLifetime > 0);
+ assertTrue(pio.preferredLifetime > 0);
+ } else if (mOldParams != null && mOldParams.prefixes.contains(prefix)) {
+ assertEquals(0, pio.validLifetime);
+ assertEquals(0, pio.preferredLifetime);
+ } else {
+ fail("Unexpected prefix: " + prefix);
+ }
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ break;
+ case ICMPV6_ND_OPTION_MTU:
+ assertEquals(1, length);
+
+ final ByteBuffer mtuBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ Struct.getSize(MtuOption.class));
+ final MtuOption mtu = Struct.parse(MtuOption.class, mtuBuf);
+ assertEquals(mNewParams.mtu, mtu.mtu);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(MtuOption.class));
+ break;
+ case ICMPV6_ND_OPTION_RDNSS:
+ final int rdnssHeaderLen = Struct.getSize(RdnssOption.class);
+ final ByteBuffer RdnssBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ rdnssHeaderLen);
+ final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf);
+ final String msg =
+ rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns";
+ final HashSet<Inet6Address> dnses =
+ rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses;
+ assertNotNull(msg, dnses);
+
+ // Check DNS servers included in this option.
+ buf.position(currentPos + rdnssHeaderLen); // skip the rdnss option header
+ final int numOfDnses = (length - 1) / 2;
+ for (int i = 0; i < numOfDnses; i++) {
+ byte[] rawAddress = new byte[IPV6_ADDR_LEN];
+ buf.get(rawAddress);
+ final Inet6Address dns =
+ (Inet6Address) InetAddress.getByAddress(rawAddress);
+ if (!dnses.contains(dns)) fail("Unexpected dns: " + dns);
+ }
+ // Unnecessary to move ByteBuffer position here, since the position has been
+ // moved forward correctly after reading DNS servers from ByteBuffer.
+ break;
+ case ICMPV6_ND_OPTION_SLLA:
+ // Do nothing, just move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(LlaOption.class));
+ break;
+ default:
+ fail("Unknown RA option type " + type);
+ }
+ }
+ return true;
+ }
+ }
+
+ private RaParams createRaParams(final String ipv6Address) throws Exception {
+ final RaParams params = new RaParams();
+ final Inet6Address address = (Inet6Address) InetAddress.getByName(ipv6Address);
+ params.dnses.add(address);
+ params.prefixes.add(new IpPrefix(address, 64));
+
+ return params;
+ }
+
+ private boolean isRaPacket(final TestRaPacket testRa, boolean multicast) throws Exception {
+ byte[] packet;
+ while ((packet = mTetheredPacketReader.poll(PACKET_TIMEOUT_MS)) != null) {
+ if (testRa.isPacketMatched(packet, multicast)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void assertUnicastRaPacket(final TestRaPacket testRa) throws Exception {
+ assertTrue(isRaPacket(testRa, false /* multicast */));
+ }
+
+ private void assertMulticastRaPacket(final TestRaPacket testRa) throws Exception {
+ assertTrue(isRaPacket(testRa, true /* multicast */));
+ }
+
+ private ByteBuffer createRsPacket(final String srcIp) throws Exception {
+ final MacAddress dstMac = MacAddress.fromString("33:33:03:04:05:06");
+ final MacAddress srcMac = mTetheredParams.macAddr;
+ final ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+
+ return Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) InetAddress.getByName(srcIp),
+ IPV6_ADDR_ALL_NODES_MULTICAST, slla);
+ }
+
+ @Test
+ public void testUnSolicitRouterAdvertisement() throws Exception {
+ assertTrue(mRaDaemon.start());
+ final RaParams params1 = createRaParams("2001:1122:3344::5566");
+ mRaDaemon.buildNewRa(null, params1);
+ assertMulticastRaPacket(new TestRaPacket(null, params1));
+
+ final RaParams params2 = createRaParams("2006:3344:5566::7788");
+ mRaDaemon.buildNewRa(params1, params2);
+ assertMulticastRaPacket(new TestRaPacket(params1, params2));
+ }
+
+ @Test
+ public void testSolicitRouterAdvertisement() throws Exception {
+ // Enable IPv6 forwarding is necessary, which makes kernel process RS correctly and
+ // create the neighbor entry for peer's link-layer address and IPv6 address. Otherwise,
+ // when device receives RS with IPv6 link-local address as source address, it has to
+ // initiate the address resolution first before responding the unicast RA.
+ sNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mTetheredParams.name, "forwarding", "1");
+
+ assertTrue(mRaDaemon.start());
+ final RaParams params1 = createRaParams("2001:1122:3344::5566");
+ mRaDaemon.buildNewRa(null, params1);
+ assertMulticastRaPacket(new TestRaPacket(null, params1));
+
+ // Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to
+ // send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local
+ // address is present).
+ final String iface = mTetheredParams.name;
+ final RouteInfo linkLocalRoute =
+ new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
+ RouteUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+
+ final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
+ mTetheredPacketReader.sendResponse(rs);
+ assertUnicastRaPacket(new TestRaPacket(null, params1));
+ }
+}
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
new file mode 100644
index 0000000..830729d
--- /dev/null
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -0,0 +1,392 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.system.OsConstants.ETH_P_IPV6;
+
+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 android.net.MacAddress;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.ArrayMap;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.NoSuchElementException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+@RunWith(AndroidJUnit4.class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+public final class BpfMapTest {
+ // Sync from packages/modules/Connectivity/Tethering/bpf_progs/offload.c.
+ private static final int TEST_MAP_SIZE = 16;
+ private static final String TETHER_DOWNSTREAM6_FS_PATH =
+ "/sys/fs/bpf/tethering/map_test_tether_downstream6_map";
+
+ private ArrayMap<TetherDownstream6Key, Tether6Value> mTestData;
+
+ private BpfMap<TetherDownstream6Key, Tether6Value> mTestMap;
+
+ @BeforeClass
+ public static void setupOnce() {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mTestData = new ArrayMap<>();
+ mTestData.put(createTetherDownstream6Key(101, "00:00:00:00:00:aa", "2001:db8::1"),
+ createTether6Value(11, "00:00:00:00:00:0a", "11:11:11:00:00:0b",
+ ETH_P_IPV6, 1280));
+ mTestData.put(createTetherDownstream6Key(102, "00:00:00:00:00:bb", "2001:db8::2"),
+ createTether6Value(22, "00:00:00:00:00:0c", "22:22:22:00:00:0d",
+ ETH_P_IPV6, 1400));
+ mTestData.put(createTetherDownstream6Key(103, "00:00:00:00:00:cc", "2001:db8::3"),
+ createTether6Value(33, "00:00:00:00:00:0e", "33:33:33:00:00:0f",
+ ETH_P_IPV6, 1500));
+
+ initTestMap();
+ }
+
+ private void initTestMap() throws Exception {
+ mTestMap = new BpfMap<>(
+ TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TetherDownstream6Key.class, Tether6Value.class);
+
+ mTestMap.forEach((key, value) -> {
+ try {
+ assertTrue(mTestMap.deleteEntry(key));
+ } catch (ErrnoException e) {
+ fail("Fail to delete the key " + key + ": " + e);
+ }
+ });
+ assertNull(mTestMap.getFirstKey());
+ assertTrue(mTestMap.isEmpty());
+ }
+
+ private TetherDownstream6Key createTetherDownstream6Key(long iif, String mac,
+ String address) throws Exception {
+ final MacAddress dstMac = MacAddress.fromString(mac);
+ final InetAddress ipv6Address = InetAddress.getByName(address);
+
+ return new TetherDownstream6Key(iif, dstMac, ipv6Address.getAddress());
+ }
+
+ private Tether6Value createTether6Value(int oif, String src, String dst, int proto, int pmtu) {
+ final MacAddress srcMac = MacAddress.fromString(src);
+ final MacAddress dstMac = MacAddress.fromString(dst);
+
+ return new Tether6Value(oif, dstMac, srcMac, proto, pmtu);
+ }
+
+ @Test
+ public void testGetFd() throws Exception {
+ try (BpfMap readOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDONLY,
+ TetherDownstream6Key.class, Tether6Value.class)) {
+ assertNotNull(readOnlyMap);
+ try {
+ readOnlyMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ fail("Writing RO map should throw ErrnoException");
+ } catch (ErrnoException expected) {
+ assertEquals(OsConstants.EPERM, expected.errno);
+ }
+ }
+ try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
+ TetherDownstream6Key.class, Tether6Value.class)) {
+ assertNotNull(writeOnlyMap);
+ try {
+ writeOnlyMap.getFirstKey();
+ fail("Reading WO map should throw ErrnoException");
+ } catch (ErrnoException expected) {
+ assertEquals(OsConstants.EPERM, expected.errno);
+ }
+ }
+ try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TetherDownstream6Key.class, Tether6Value.class)) {
+ assertNotNull(readWriteMap);
+ }
+ }
+
+ @Test
+ public void testIsEmpty() throws Exception {
+ assertNull(mTestMap.getFirstKey());
+ assertTrue(mTestMap.isEmpty());
+
+ mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ assertFalse(mTestMap.isEmpty());
+
+ mTestMap.deleteEntry((mTestData.keyAt(0)));
+ assertTrue(mTestMap.isEmpty());
+ }
+
+ @Test
+ public void testGetFirstKey() throws Exception {
+ // getFirstKey on an empty map returns null.
+ assertFalse(mTestMap.containsKey(mTestData.keyAt(0)));
+ assertNull(mTestMap.getFirstKey());
+ assertNull(mTestMap.getValue(mTestData.keyAt(0)));
+
+ // getFirstKey on a non-empty map returns the first key.
+ mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ assertEquals(mTestData.keyAt(0), mTestMap.getFirstKey());
+ }
+
+ @Test
+ public void testGetNextKey() throws Exception {
+ // [1] If the passed-in key is not found on empty map, return null.
+ final TetherDownstream6Key nonexistentKey =
+ createTetherDownstream6Key(1234, "00:00:00:00:00:01", "2001:db8::10");
+ assertNull(mTestMap.getNextKey(nonexistentKey));
+
+ // [2] If the passed-in key is null on empty map, throw NullPointerException.
+ try {
+ mTestMap.getNextKey(null);
+ fail("Getting next key with null key should throw NullPointerException");
+ } catch (NullPointerException expected) { }
+
+ // The BPF map has one entry now.
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>();
+ mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ resultMap.put(mTestData.keyAt(0), mTestData.valueAt(0));
+
+ // [3] If the passed-in key is the last key, return null.
+ // Because there is only one entry in the map, the first key equals the last key.
+ final TetherDownstream6Key lastKey = mTestMap.getFirstKey();
+ assertNull(mTestMap.getNextKey(lastKey));
+
+ // The BPF map has two entries now.
+ mTestMap.insertEntry(mTestData.keyAt(1), mTestData.valueAt(1));
+ resultMap.put(mTestData.keyAt(1), mTestData.valueAt(1));
+
+ // [4] If the passed-in key is found, return the next key.
+ TetherDownstream6Key nextKey = mTestMap.getFirstKey();
+ while (nextKey != null) {
+ if (resultMap.remove(nextKey).equals(nextKey)) {
+ fail("Unexpected result: " + nextKey);
+ }
+ nextKey = mTestMap.getNextKey(nextKey);
+ }
+ assertTrue(resultMap.isEmpty());
+
+ // [5] If the passed-in key is not found on non-empty map, return the first key.
+ assertEquals(mTestMap.getFirstKey(), mTestMap.getNextKey(nonexistentKey));
+
+ // [6] If the passed-in key is null on non-empty map, throw NullPointerException.
+ try {
+ mTestMap.getNextKey(null);
+ fail("Getting next key with null key should throw NullPointerException");
+ } catch (NullPointerException expected) { }
+ }
+
+ @Test
+ public void testUpdateEntry() throws Exception {
+ final TetherDownstream6Key key = mTestData.keyAt(0);
+ final Tether6Value value = mTestData.valueAt(0);
+ final Tether6Value value2 = mTestData.valueAt(1);
+ assertFalse(mTestMap.deleteEntry(key));
+
+ // updateEntry will create an entry if it does not exist already.
+ mTestMap.updateEntry(key, value);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result = mTestMap.getValue(key);
+ assertEquals(value, result);
+
+ // updateEntry will update an entry that already exists.
+ mTestMap.updateEntry(key, value2);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result2 = mTestMap.getValue(key);
+ assertEquals(value2, result2);
+
+ assertTrue(mTestMap.deleteEntry(key));
+ assertFalse(mTestMap.containsKey(key));
+ }
+
+ @Test
+ public void testInsertOrReplaceEntry() throws Exception {
+ final TetherDownstream6Key key = mTestData.keyAt(0);
+ final Tether6Value value = mTestData.valueAt(0);
+ final Tether6Value value2 = mTestData.valueAt(1);
+ assertFalse(mTestMap.deleteEntry(key));
+
+ // insertOrReplaceEntry will create an entry if it does not exist already.
+ assertTrue(mTestMap.insertOrReplaceEntry(key, value));
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result = mTestMap.getValue(key);
+ assertEquals(value, result);
+
+ // updateEntry will update an entry that already exists.
+ assertFalse(mTestMap.insertOrReplaceEntry(key, value2));
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result2 = mTestMap.getValue(key);
+ assertEquals(value2, result2);
+
+ assertTrue(mTestMap.deleteEntry(key));
+ assertFalse(mTestMap.containsKey(key));
+ }
+
+ @Test
+ public void testInsertReplaceEntry() throws Exception {
+ final TetherDownstream6Key key = mTestData.keyAt(0);
+ final Tether6Value value = mTestData.valueAt(0);
+ final Tether6Value value2 = mTestData.valueAt(1);
+
+ try {
+ mTestMap.replaceEntry(key, value);
+ fail("Replacing non-existent key " + key + " should throw NoSuchElementException");
+ } catch (NoSuchElementException expected) { }
+ assertFalse(mTestMap.containsKey(key));
+
+ mTestMap.insertEntry(key, value);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result = mTestMap.getValue(key);
+ assertEquals(value, result);
+ try {
+ mTestMap.insertEntry(key, value);
+ fail("Inserting existing key " + key + " should throw IllegalStateException");
+ } catch (IllegalStateException expected) { }
+
+ mTestMap.replaceEntry(key, value2);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result2 = mTestMap.getValue(key);
+ assertEquals(value2, result2);
+ }
+
+ @Test
+ public void testIterateBpfMap() throws Exception {
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>(mTestData);
+
+ for (int i = 0; i < resultMap.size(); i++) {
+ mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+ }
+
+ mTestMap.forEach((key, value) -> {
+ if (!value.equals(resultMap.remove(key))) {
+ fail("Unexpected result: " + key + ", value: " + value);
+ }
+ });
+ assertTrue(resultMap.isEmpty());
+ }
+
+ @Test
+ public void testIterateEmptyMap() throws Exception {
+ // Can't use an int because variables used in a lambda must be final.
+ final AtomicInteger count = new AtomicInteger();
+ mTestMap.forEach((key, value) -> count.incrementAndGet());
+ // Expect that the consumer was never called.
+ assertEquals(0, count.get());
+ }
+
+ @Test
+ public void testIterateDeletion() throws Exception {
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>(mTestData);
+
+ for (int i = 0; i < resultMap.size(); i++) {
+ mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+ }
+
+ // Can't use an int because variables used in a lambda must be final.
+ final AtomicInteger count = new AtomicInteger();
+ mTestMap.forEach((key, value) -> {
+ try {
+ assertTrue(mTestMap.deleteEntry(key));
+ } catch (ErrnoException e) {
+ fail("Fail to delete key " + key + ": " + e);
+ }
+ if (!value.equals(resultMap.remove(key))) {
+ fail("Unexpected result: " + key + ", value: " + value);
+ }
+ count.incrementAndGet();
+ });
+ assertEquals(3, count.get());
+ assertTrue(resultMap.isEmpty());
+ assertNull(mTestMap.getFirstKey());
+ }
+
+ @Test
+ public void testClear() throws Exception {
+ // Clear an empty map.
+ assertTrue(mTestMap.isEmpty());
+ mTestMap.clear();
+
+ // Clear a map with some data in it.
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>(mTestData);
+ for (int i = 0; i < resultMap.size(); i++) {
+ mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+ }
+ assertFalse(mTestMap.isEmpty());
+ mTestMap.clear();
+ assertTrue(mTestMap.isEmpty());
+
+ // Clearing an already-closed map throws.
+ mTestMap.close();
+ try {
+ mTestMap.clear();
+ fail("clearing already-closed map should throw");
+ } catch (ErrnoException expected) {
+ assertEquals(OsConstants.EBADF, expected.errno);
+ }
+ }
+
+ @Test
+ public void testInsertOverflow() throws Exception {
+ final ArrayMap<TetherDownstream6Key, Tether6Value> testData =
+ new ArrayMap<>();
+
+ // Build test data for TEST_MAP_SIZE + 1 entries.
+ for (int i = 1; i <= TEST_MAP_SIZE + 1; i++) {
+ testData.put(
+ createTetherDownstream6Key(i, "00:00:00:00:00:01", "2001:db8::1"),
+ createTether6Value(100, "de:ad:be:ef:00:01", "de:ad:be:ef:00:02",
+ ETH_P_IPV6, 1500));
+ }
+
+ // Insert #TEST_MAP_SIZE test entries to the map. The map has reached the limit.
+ for (int i = 0; i < TEST_MAP_SIZE; i++) {
+ mTestMap.insertEntry(testData.keyAt(i), testData.valueAt(i));
+ }
+
+ // The map won't allow inserting any more entries.
+ try {
+ mTestMap.insertEntry(testData.keyAt(TEST_MAP_SIZE), testData.valueAt(TEST_MAP_SIZE));
+ fail("Writing too many entries should throw ErrnoException");
+ } catch (ErrnoException expected) {
+ // Expect that can't insert the entry anymore because the number of elements in the
+ // map reached the limit. See man-pages/bpf.
+ assertEquals(OsConstants.E2BIG, expected.errno);
+ }
+ }
+}
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
new file mode 100644
index 0000000..57c28fc
--- /dev/null
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.netlink.NetlinkSocket.DEFAULT_RECV_BUFSIZE;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_GET;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_NEW;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NFNL_SUBSYS_CTNETLINK;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_DESTROY;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_NEW;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.netlink.StructNlMsgHdr;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.NativeHandle;
+import android.system.Os;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackSocketTest {
+ private static final long TIMEOUT = 500;
+
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private final SharedLog mLog = new SharedLog("privileged-test");
+
+ private OffloadHardwareInterface mOffloadHw;
+ private OffloadHardwareInterface.Dependencies mDeps;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+ if (Looper.myLooper() == null) Looper.prepare();
+
+ mDeps = new OffloadHardwareInterface.Dependencies(mLog);
+ mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
+ }
+
+ @Test
+ public void testIpv4ConntrackSocket() throws Exception {
+ // Set up server and connect.
+ final InetSocketAddress anyAddress = new InetSocketAddress(
+ InetAddress.getByName("127.0.0.1"), 0);
+ final ServerSocket serverSocket = new ServerSocket();
+ serverSocket.bind(anyAddress);
+ final SocketAddress theAddress = serverSocket.getLocalSocketAddress();
+
+ // Make a connection to the server.
+ final Socket socket = new Socket();
+ socket.connect(theAddress);
+ final Socket acceptedSocket = serverSocket.accept();
+
+ final NativeHandle handle = mDeps.createConntrackSocket(
+ NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
+ mOffloadHw.sendIpv4NfGenMsg(handle,
+ (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
+ (short) (NLM_F_REQUEST | NLM_F_DUMP));
+
+ boolean foundConntrackEntry = false;
+ ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_RECV_BUFSIZE);
+ buffer.order(ByteOrder.nativeOrder());
+
+ try {
+ while (Os.read(handle.getFileDescriptor(), buffer) > 0) {
+ buffer.flip();
+
+ // TODO: ConntrackMessage should get a parse API like StructNlMsgHdr
+ // so we can confirm that the conntrack added is for the TCP connection above.
+ final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(buffer);
+ assertNotNull(nlmsghdr);
+
+ // As long as 1 conntrack entry is found test case will pass, even if it's not
+ // the from the TCP connection above.
+ if (nlmsghdr.nlmsg_type == ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_NEW)) {
+ foundConntrackEntry = true;
+ break;
+ }
+ }
+ } finally {
+ socket.close();
+ serverSocket.close();
+ }
+ assertTrue("Did not receive any NFNL_SUBSYS_CTNETLINK/IPCTNL_MSG_CT_NEW message",
+ foundConntrackEntry);
+ }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
new file mode 100644
index 0000000..c6f19d7
--- /dev/null
+++ b/Tethering/tests/unit/Android.bp
@@ -0,0 +1,104 @@
+//
+// 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.
+//
+
+// Tests in this folder are included both in unit tests and CTS.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "TetheringCommonTests",
+ srcs: [
+ "common/**/*.java",
+ "common/**/*.kt"
+ ],
+ static_libs: [
+ "androidx.test.rules",
+ "net-tests-utils",
+ ],
+ // TODO(b/147200698) change sdk_version to module-current and remove framework-minus-apex
+ sdk_version: "core_platform",
+ libs: [
+ "framework-minus-apex",
+ "framework-connectivity.impl",
+ "framework-tethering.impl",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity/tests/cts/tethering",
+ ],
+}
+
+java_defaults {
+ name: "TetheringTestsDefaults",
+ min_sdk_version: "30",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "TetheringApiCurrentLib",
+ "TetheringCommonTests",
+ "androidx.test.rules",
+ "frameworks-base-testutils",
+ "mockito-target-extended-minus-junit4",
+ "net-tests-utils",
+ "testables",
+ ],
+ // TODO(b/147200698) change sdk_version to module-current and
+ // remove framework-minus-apex, ext, and framework-res
+ sdk_version: "core_platform",
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ "ext",
+ "framework-minus-apex",
+ "framework-res",
+ "framework-connectivity.impl",
+ "framework-tethering.impl",
+ "framework-wifi.stubs.module_lib",
+ ],
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ "libtetherutilsjni",
+ ],
+}
+
+// Library containing the unit tests. This is used by the coverage test target to pull in the
+// unit test code. It is not currently used by the tests themselves because all the build
+// configuration needed by the tests is in the TetheringTestsDefaults rule.
+android_library {
+ name: "TetheringTestsLatestSdkLib",
+ defaults: ["TetheringTestsDefaults"],
+ target_sdk_version: "30",
+ visibility: [
+ "//packages/modules/Connectivity/Tethering/tests/integration",
+ ]
+}
+
+android_test {
+ name: "TetheringTests",
+ platform_apis: true,
+ test_suites: [
+ "device-tests",
+ "mts",
+ ],
+ jarjar_rules: ":TetheringTestsJarJarRules",
+ defaults: ["TetheringTestsDefaults"],
+ compile_multilib: "both",
+}
diff --git a/Tethering/tests/unit/AndroidManifest.xml b/Tethering/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..355342f
--- /dev/null
+++ b/Tethering/tests/unit/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.networkstack.tethering.tests.unit">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ <service
+ android:name="com.android.networkstack.tethering.MockTetheringService"
+ android:permission="android.permission.TETHER_PRIVILEGED"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.networkstack.tethering.TetheringService"/>
+ </intent-filter>
+ </service>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.networkstack.tethering.tests.unit"
+ android:label="Tethering service tests">
+ </instrumentation>
+</manifest>
diff --git a/Tethering/tests/unit/common/android/net/TetheredClientTest.kt b/Tethering/tests/unit/common/android/net/TetheredClientTest.kt
new file mode 100644
index 0000000..55c59dd
--- /dev/null
+++ b/Tethering/tests/unit/common/android/net/TetheredClientTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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 android.net
+
+import android.net.InetAddresses.parseNumericAddress
+import android.net.TetheredClient.AddressInfo
+import android.net.TetheringManager.TETHERING_BLUETOOTH
+import android.net.TetheringManager.TETHERING_USB
+import android.system.OsConstants.RT_SCOPE_UNIVERSE
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+private val TEST_MACADDR = MacAddress.fromBytes(byteArrayOf(12, 23, 34, 45, 56, 67))
+private val TEST_OTHER_MACADDR = MacAddress.fromBytes(byteArrayOf(23, 34, 45, 56, 67, 78))
+private val TEST_ADDR1 = makeLinkAddress("192.168.113.3", prefixLength = 24, expTime = 123L)
+private val TEST_ADDR2 = makeLinkAddress("fe80::1:2:3", prefixLength = 64, expTime = 456L)
+private val TEST_HOSTNAME = "test_hostname"
+private val TEST_OTHER_HOSTNAME = "test_other_hostname"
+private val TEST_ADDRINFO1 = AddressInfo(TEST_ADDR1, TEST_HOSTNAME)
+private val TEST_ADDRINFO2 = AddressInfo(TEST_ADDR2, null)
+
+private fun makeLinkAddress(addr: String, prefixLength: Int, expTime: Long) = LinkAddress(
+ parseNumericAddress(addr),
+ prefixLength,
+ 0 /* flags */,
+ RT_SCOPE_UNIVERSE,
+ expTime /* deprecationTime */,
+ expTime /* expirationTime */)
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TetheredClientTest {
+ @Test
+ fun testParceling() {
+ assertParcelSane(TEST_ADDRINFO1, fieldCount = 2)
+ assertParcelSane(makeTestClient(), fieldCount = 3)
+ }
+
+ @Test
+ fun testEquals() {
+ assertEquals(makeTestClient(), makeTestClient())
+
+ // Different mac address
+ assertNotEquals(makeTestClient(), TetheredClient(
+ TEST_OTHER_MACADDR,
+ listOf(TEST_ADDRINFO1, TEST_ADDRINFO2),
+ TETHERING_BLUETOOTH))
+
+ // Different hostname
+ assertNotEquals(makeTestClient(), TetheredClient(
+ TEST_MACADDR,
+ listOf(AddressInfo(TEST_ADDR1, TEST_OTHER_HOSTNAME), TEST_ADDRINFO2),
+ TETHERING_BLUETOOTH))
+
+ // Null hostname
+ assertNotEquals(makeTestClient(), TetheredClient(
+ TEST_MACADDR,
+ listOf(AddressInfo(TEST_ADDR1, null), TEST_ADDRINFO2),
+ TETHERING_BLUETOOTH))
+
+ // Missing address
+ assertNotEquals(makeTestClient(), TetheredClient(
+ TEST_MACADDR,
+ listOf(TEST_ADDRINFO2),
+ TETHERING_BLUETOOTH))
+
+ // Different type
+ assertNotEquals(makeTestClient(), TetheredClient(
+ TEST_MACADDR,
+ listOf(TEST_ADDRINFO1, TEST_ADDRINFO2),
+ TETHERING_USB))
+ }
+
+ @Test
+ fun testAddAddresses() {
+ val client1 = TetheredClient(TEST_MACADDR, listOf(TEST_ADDRINFO1), TETHERING_USB)
+ val client2 = TetheredClient(TEST_OTHER_MACADDR, listOf(TEST_ADDRINFO2), TETHERING_USB)
+ assertEquals(TetheredClient(
+ TEST_MACADDR,
+ listOf(TEST_ADDRINFO1, TEST_ADDRINFO2),
+ TETHERING_USB), client1.addAddresses(client2))
+ }
+
+ @Test
+ fun testGetters() {
+ assertEquals(TEST_MACADDR, makeTestClient().macAddress)
+ assertEquals(listOf(TEST_ADDRINFO1, TEST_ADDRINFO2), makeTestClient().addresses)
+ assertEquals(TETHERING_BLUETOOTH, makeTestClient().tetheringType)
+ }
+
+ @Test
+ fun testAddressInfo_Getters() {
+ assertEquals(TEST_ADDR1, TEST_ADDRINFO1.address)
+ assertEquals(TEST_ADDR2, TEST_ADDRINFO2.address)
+ assertEquals(TEST_HOSTNAME, TEST_ADDRINFO1.hostname)
+ assertEquals(null, TEST_ADDRINFO2.hostname)
+ }
+
+ private fun makeTestClient() = TetheredClient(
+ TEST_MACADDR,
+ listOf(TEST_ADDRINFO1, TEST_ADDRINFO2),
+ TETHERING_BLUETOOTH)
+}
\ No newline at end of file
diff --git a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
new file mode 100644
index 0000000..a8857b2
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.dhcp;
+
+import static android.net.InetAddresses.parseNumericAddress;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.LinkAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpServingParamsParcelExtTest {
+ private static final Inet4Address TEST_ADDRESS = inet4Addr("192.168.0.123");
+ private static final Inet4Address TEST_CLIENT_ADDRESS = inet4Addr("192.168.0.42");
+ private static final int TEST_ADDRESS_PARCELED = 0xc0a8007b;
+ private static final int TEST_CLIENT_ADDRESS_PARCELED = 0xc0a8002a;
+ private static final int TEST_PREFIX_LENGTH = 17;
+ private static final int TEST_LEASE_TIME_SECS = 120;
+ private static final int TEST_MTU = 1000;
+ private static final Set<Inet4Address> TEST_ADDRESS_SET =
+ new HashSet<Inet4Address>(Arrays.asList(
+ new Inet4Address[] {inet4Addr("192.168.1.123"), inet4Addr("192.168.1.124")}));
+ private static final Set<Integer> TEST_ADDRESS_SET_PARCELED =
+ new HashSet<Integer>(Arrays.asList(new Integer[] {0xc0a8017b, 0xc0a8017c}));
+
+ private DhcpServingParamsParcelExt mParcel;
+
+ @Before
+ public void setUp() {
+ mParcel = new DhcpServingParamsParcelExt();
+ }
+
+ @Test
+ public void testSetServerAddr() {
+ mParcel.setServerAddr(new LinkAddress(TEST_ADDRESS, TEST_PREFIX_LENGTH));
+
+ assertEquals(TEST_ADDRESS_PARCELED, mParcel.serverAddr);
+ assertEquals(TEST_PREFIX_LENGTH, mParcel.serverAddrPrefixLength);
+ }
+
+ @Test
+ public void testSetDefaultRouters() {
+ mParcel.setDefaultRouters(TEST_ADDRESS_SET);
+ assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.defaultRouters));
+ }
+
+ @Test
+ public void testSetDnsServers() {
+ mParcel.setDnsServers(TEST_ADDRESS_SET);
+ assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.dnsServers));
+ }
+
+ @Test
+ public void testSetExcludedAddrs() {
+ mParcel.setExcludedAddrs(TEST_ADDRESS_SET);
+ assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.excludedAddrs));
+ }
+
+ @Test
+ public void testSetDhcpLeaseTimeSecs() {
+ mParcel.setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS);
+ assertEquals(TEST_LEASE_TIME_SECS, mParcel.dhcpLeaseTimeSecs);
+ }
+
+ @Test
+ public void testSetLinkMtu() {
+ mParcel.setLinkMtu(TEST_MTU);
+ assertEquals(TEST_MTU, mParcel.linkMtu);
+ }
+
+ @Test
+ public void testSetMetered() {
+ mParcel.setMetered(true);
+ assertTrue(mParcel.metered);
+ mParcel.setMetered(false);
+ assertFalse(mParcel.metered);
+ }
+
+ @Test
+ public void testSetClientAddr() {
+ mParcel.setSingleClientAddr(TEST_CLIENT_ADDRESS);
+ assertEquals(TEST_CLIENT_ADDRESS_PARCELED, mParcel.singleClientAddr);
+ }
+
+ private static Inet4Address inet4Addr(String addr) {
+ return (Inet4Address) parseNumericAddress(addr);
+ }
+
+ private static Set<Integer> asSet(int[] ints) {
+ return IntStream.of(ints).boxed().collect(Collectors.toSet());
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
new file mode 100644
index 0000000..ce69cb3
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -0,0 +1,1455 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+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;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.ip.IpServer.STATE_AVAILABLE;
+import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
+import static android.net.ip.IpServer.STATE_TETHERED;
+import static android.net.ip.IpServer.STATE_UNAVAILABLE;
+import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static android.net.netlink.NetlinkConstants.RTM_NEWNEIGH;
+import static android.net.netlink.StructNdMsg.NUD_FAILED;
+import static android.net.netlink.StructNdMsg.NUD_REACHABLE;
+import static android.net.netlink.StructNdMsg.NUD_STALE;
+import static android.system.OsConstants.ETH_P_IPV6;
+
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+
+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.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+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.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.NetworkStatsManager;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.RouteInfo;
+import android.net.TetherOffloadRuleParcel;
+import android.net.TetherStatsParcel;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpEventCallbacks;
+import android.net.dhcp.IDhcpServer;
+import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.ip.IpNeighborMonitor.NeighborEvent;
+import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.util.InterfaceParams;
+import android.net.util.InterfaceSet;
+import android.net.util.PrefixUtils;
+import android.net.util.SharedLog;
+import android.os.Build;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.networkstack.tethering.BpfCoordinator;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfMap;
+import com.android.networkstack.tethering.PrivateAddressCoordinator;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.Tether6Value;
+import com.android.networkstack.tethering.TetherDevKey;
+import com.android.networkstack.tethering.TetherDevValue;
+import com.android.networkstack.tethering.TetherDownstream6Key;
+import com.android.networkstack.tethering.TetherLimitKey;
+import com.android.networkstack.tethering.TetherLimitValue;
+import com.android.networkstack.tethering.TetherStatsKey;
+import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream6Key;
+import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Captor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpServerTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final String IFACE_NAME = "testnet1";
+ private static final String UPSTREAM_IFACE = "upstream0";
+ private static final String UPSTREAM_IFACE2 = "upstream1";
+ private static final int UPSTREAM_IFINDEX = 101;
+ private static final int UPSTREAM_IFINDEX2 = 102;
+ private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1";
+ private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24;
+ private static final int DHCP_LEASE_TIME_SECS = 3600;
+ private static final boolean DEFAULT_USING_BPF_OFFLOAD = true;
+
+ private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
+ IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
+ private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
+ UPSTREAM_IFACE, UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
+ private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
+ UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.ALL_ZEROS_ADDRESS,
+ 1500 /* defaultMtu */);
+
+ private static final int MAKE_DHCPSERVER_TIMEOUT_MS = 1000;
+
+ private final LinkAddress mTestAddress = new LinkAddress("192.168.42.5/24");
+ private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
+
+ @Mock private INetd mNetd;
+ @Mock private IpServer.Callback mCallback;
+ @Mock private SharedLog mSharedLog;
+ @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;
+ @Mock private NetworkStatsManager mStatsManager;
+ @Mock private TetheringConfiguration mTetherConfig;
+ @Mock private ConntrackMonitor mConntrackMonitor;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+ @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+ @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+ @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+ @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
+ @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
+
+ @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
+
+ private final TestLooper mLooper = new TestLooper();
+ private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
+ ArgumentCaptor.forClass(LinkProperties.class);
+ private IpServer mIpServer;
+ private InterfaceConfigurationParcel mInterfaceConfiguration;
+ private NeighborEventConsumer mNeighborEventConsumer;
+ private BpfCoordinator mBpfCoordinator;
+ private BpfCoordinator.Dependencies mBpfDeps;
+
+ private void initStateMachine(int interfaceType) throws Exception {
+ initStateMachine(interfaceType, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
+ }
+
+ private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
+ boolean usingBpfOffload) throws Exception {
+ when(mDependencies.getDadProxy(any(), any())).thenReturn(mDadProxy);
+ when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
+ when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS);
+ when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
+ when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
+
+ mInterfaceConfiguration = new InterfaceConfigurationParcel();
+ mInterfaceConfiguration.flags = new String[0];
+ if (interfaceType == TETHERING_BLUETOOTH) {
+ mInterfaceConfiguration.ipv4Addr = BLUETOOTH_IFACE_ADDR;
+ mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH;
+ }
+
+ ArgumentCaptor<NeighborEventConsumer> neighborCaptor =
+ ArgumentCaptor.forClass(NeighborEventConsumer.class);
+ doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(),
+ neighborCaptor.capture());
+
+ mIpServer = new IpServer(
+ IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
+ mCallback, usingLegacyDhcp, usingBpfOffload, mAddressCoordinator, mDependencies);
+ mIpServer.start();
+ mNeighborEventConsumer = neighborCaptor.getValue();
+
+ // 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);
+
+ when(mRaDaemon.start()).thenReturn(true);
+ }
+
+ private void initTetheredStateMachine(int interfaceType, String upstreamIface)
+ throws Exception {
+ initTetheredStateMachine(interfaceType, upstreamIface, false,
+ DEFAULT_USING_BPF_OFFLOAD);
+ }
+
+ private void initTetheredStateMachine(int interfaceType, String upstreamIface,
+ boolean usingLegacyDhcp, boolean usingBpfOffload) throws Exception {
+ initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ if (upstreamIface != null) {
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(upstreamIface);
+ dispatchTetherConnectionChanged(upstreamIface, lp, 0);
+ }
+ reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
+ mTestAddress);
+ }
+
+ private void setUpDhcpServer() throws Exception {
+ doAnswer(inv -> {
+ final IDhcpServerCallbacks cb = inv.getArgument(2);
+ new Thread(() -> {
+ try {
+ cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }).run();
+ return null;
+ }).when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(), any());
+ }
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
+ mTestAddress);
+ when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
+
+ mBpfDeps = new BpfCoordinator.Dependencies() {
+ @NonNull
+ public Handler getHandler() {
+ return new Handler(mLooper.getLooper());
+ }
+
+ @NonNull
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ @NonNull
+ public NetworkStatsManager getNetworkStatsManager() {
+ return mStatsManager;
+ }
+
+ @NonNull
+ public SharedLog getSharedLog() {
+ return mSharedLog;
+ }
+
+ @Nullable
+ public TetheringConfiguration getTetherConfig() {
+ return mTetherConfig;
+ }
+
+ @NonNull
+ public ConntrackMonitor getConntrackMonitor(
+ ConntrackMonitor.ConntrackEventConsumer consumer) {
+ return mConntrackMonitor;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+ return mBpfDownstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+ return mBpfUpstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+ return mBpfDownstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+ return mBpfUpstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ return mBpfStatsMap;
+ }
+
+ @Nullable
+ public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+ return mBpfLimitMap;
+ }
+
+ @Nullable
+ public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+ return mBpfDevMap;
+ }
+ };
+ mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
+
+ setUpDhcpServer();
+ }
+
+ @Test
+ public void startsOutAvailable() {
+ when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
+ .thenReturn(mIpNeighborMonitor);
+ mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
+ mNetd, mBpfCoordinator, mCallback, false /* usingLegacyDhcp */,
+ DEFAULT_USING_BPF_OFFLOAD, mAddressCoordinator, mDependencies);
+ mIpServer.start();
+ mLooper.dispatchAll();
+ verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ verify(mCallback).updateLinkProperties(eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mCallback, mNetd);
+ }
+
+ @Test
+ public void shouldDoNothingUntilRequested() throws Exception {
+ initStateMachine(TETHERING_BLUETOOTH);
+ final int [] noOp_commands = {
+ IpServer.CMD_TETHER_UNREQUESTED,
+ IpServer.CMD_IP_FORWARDING_ENABLE_ERROR,
+ IpServer.CMD_IP_FORWARDING_DISABLE_ERROR,
+ IpServer.CMD_START_TETHERING_ERROR,
+ IpServer.CMD_STOP_TETHERING_ERROR,
+ IpServer.CMD_SET_DNS_FORWARDERS_ERROR,
+ IpServer.CMD_TETHER_CONNECTION_CHANGED
+ };
+ for (int command : noOp_commands) {
+ // None of these commands should trigger us to request action from
+ // the rest of the system.
+ dispatchCommand(command);
+ verifyNoMoreInteractions(mNetd, mCallback);
+ }
+ }
+
+ @Test
+ public void handlesImmediateInterfaceDown() throws Exception {
+ initStateMachine(TETHERING_BLUETOOTH);
+
+ dispatchCommand(IpServer.CMD_INTERFACE_DOWN);
+ verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+ verify(mCallback).updateLinkProperties(eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNetd, mCallback);
+ }
+
+ @Test
+ public void canBeTethered() throws Exception {
+ initStateMachine(TETHERING_BLUETOOTH);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ InOrder inOrder = inOrder(mCallback, mNetd);
+ inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+ inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ // One for ipv4 route, one for ipv6 link local route.
+ inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
+ any(), any());
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNetd, mCallback);
+ }
+
+ @Test
+ public void canUnrequestTethering() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, null);
+
+ dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+ InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+ inOrder.verify(mNetd).tetherApplyDnsInterfaces();
+ inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
+ inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ }
+
+ @Test
+ public void canBeTetheredAsUsb() throws Exception {
+ initStateMachine(TETHERING_USB);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+ IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+ inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+ inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
+ any(), any());
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
+ verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ }
+
+ @Test
+ public void canBeTetheredAsWifiP2p() throws Exception {
+ initStateMachine(TETHERING_WIFI_P2P);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+ IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP)));
+ inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+ inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
+ any(), any());
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
+ verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ }
+
+ @Test
+ public void handlesFirstUpstreamChange() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, null);
+
+ // Telling the state machine about its upstream interface triggers
+ // a little more configuration.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(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);
+
+ verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+ }
+
+ @Test
+ public void handlesChangingUpstream() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // 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);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(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);
+
+ verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+ }
+
+ @Test
+ public void handlesChangingUpstreamNatFailure() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // 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);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
+ // tetherAddForward.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ UPSTREAM_IFACE2);
+ inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNetd).tetherAddForward(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);
+ }
+
+ @Test
+ public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ doThrow(RemoteException.class).when(mNetd).ipfwdAddInterfaceForward(
+ IFACE_NAME, UPSTREAM_IFACE2);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // 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);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
+ // ipfwdAddInterfaceForward.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(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);
+
+ // 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);
+ }
+
+ @Test
+ public void canUnrequestTetheringWithUpstream() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+
+ dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+ InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+ 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).tetherOffloadRuleClear(mIpServer);
+ inOrder.verify(mNetd).tetherApplyDnsInterfaces();
+ inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
+ inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+ inOrder.verify(mBpfCoordinator).stopMonitoring(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);
+ }
+
+ @Test
+ public void interfaceDownLeadsToUnavailable() throws Exception {
+ for (boolean shouldThrow : new boolean[]{true, false}) {
+ initTetheredStateMachine(TETHERING_USB, null);
+
+ if (shouldThrow) {
+ doThrow(RemoteException.class).when(mNetd).tetherInterfaceRemove(IFACE_NAME);
+ }
+ dispatchCommand(IpServer.CMD_INTERFACE_DOWN);
+ InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
+ // Currently IpServer interfaceSetCfg twice to stop IPv4. One just set interface down
+ // Another one is set IPv4 to 0.0.0.0/0 as clearng ipv4 address.
+ usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
+ argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ usbTeardownOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+ usbTeardownOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ }
+ }
+
+ @Test
+ public void usbShouldBeTornDownOnTetherError() throws Exception {
+ initStateMachine(TETHERING_USB);
+
+ doThrow(RemoteException.class).when(mNetd).tetherInterfaceAdd(IFACE_NAME);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
+ usbTeardownOrder.verify(mNetd).interfaceSetCfg(
+ argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ usbTeardownOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+
+ usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
+ argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ usbTeardownOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+ usbTeardownOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ }
+
+ @Test
+ public void shouldTearDownUsbOnUpstreamError() throws Exception {
+ initTetheredStateMachine(TETHERING_USB, null);
+
+ doThrow(RemoteException.class).when(mNetd).tetherAddForward(anyString(), anyString());
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
+ usbTeardownOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
+
+ usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
+ argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ usbTeardownOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_ENABLE_FORWARDING_ERROR);
+ usbTeardownOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ }
+
+ @Test
+ public void ignoresDuplicateUpstreamNotifications() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ verifyNoMoreInteractions(mNetd, mCallback);
+
+ for (int i = 0; i < 5; i++) {
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ verifyNoMoreInteractions(mNetd, mCallback);
+ }
+ }
+
+ @Test
+ public void startsDhcpServer() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress));
+ }
+
+ @Test
+ public void startsDhcpServerOnBluetooth() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(mBluetoothPrefix);
+ }
+
+ @Test
+ public void startsDhcpServerOnWifiP2p() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI_P2P, UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress));
+ }
+
+ @Test
+ public void startsDhcpServerOnNcm() throws Exception {
+ initStateMachine(TETHERING_NCM);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(new IpPrefix("192.168.42.0/24"));
+ }
+
+ @Test
+ public void testOnNewPrefixRequest() throws Exception {
+ initStateMachine(TETHERING_NCM);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+
+ final IDhcpEventCallbacks eventCallbacks;
+ final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+ ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+ verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), dhcpEventCbsCaptor.capture());
+ eventCallbacks = dhcpEventCbsCaptor.getValue();
+ assertDhcpStarted(new IpPrefix("192.168.42.0/24"));
+
+ final ArgumentCaptor<LinkProperties> lpCaptor =
+ ArgumentCaptor.forClass(LinkProperties.class);
+ InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator);
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ // One for ipv4 route, one for ipv6 link local route.
+ inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
+ any(), any());
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture());
+ verifyNoMoreInteractions(mCallback, mAddressCoordinator);
+
+ // Simulate the DHCP server receives DHCPDECLINE on MirrorLink and then signals
+ // onNewPrefixRequest callback.
+ final LinkAddress newAddress = new LinkAddress("192.168.100.125/24");
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
+ newAddress);
+ eventCallbacks.onNewPrefixRequest(new IpPrefix("192.168.42.0/24"));
+ mLooper.dispatchAll();
+
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(false));
+ inOrder.verify(mNetd).tetherApplyDnsInterfaces();
+ inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture());
+ verifyNoMoreInteractions(mCallback);
+
+ final LinkProperties linkProperties = lpCaptor.getValue();
+ final List<LinkAddress> linkAddresses = linkProperties.getLinkAddresses();
+ assertEquals(1, linkProperties.getLinkAddresses().size());
+ assertEquals(1, linkProperties.getRoutes().size());
+ final IpPrefix prefix = new IpPrefix(linkAddresses.get(0).getAddress(),
+ linkAddresses.get(0).getPrefixLength());
+ assertNotEquals(prefix, new IpPrefix("192.168.42.0/24"));
+
+ verify(mDhcpServer).updateParams(mDhcpParamsCaptor.capture(), any());
+ assertDhcpServingParams(mDhcpParamsCaptor.getValue(), prefix);
+ }
+
+ @Test
+ public void doesNotStartDhcpServerIfDisabled() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */,
+ DEFAULT_USING_BPF_OFFLOAD);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ verify(mDependencies, never()).makeDhcpServer(any(), any(), any());
+ }
+
+ private InetAddress addr(String addr) throws Exception {
+ return InetAddresses.parseNumericAddress(addr);
+ }
+
+ private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
+ mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
+ nudState, mac));
+ mLooper.dispatchAll();
+ }
+
+ private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
+ mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
+ nudState, mac));
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Custom ArgumentMatcher for TetherOffloadRuleParcel. This is needed because generated stable
+ * AIDL classes don't have equals(), so we cannot just use eq(). A custom assert, such as:
+ *
+ * private void checkFooCalled(StableParcelable p, ...) {
+ * ArgumentCaptor<FooParam> captor = ArgumentCaptor.forClass(FooParam.class);
+ * verify(mMock).foo(captor.capture());
+ * Foo foo = captor.getValue();
+ * assertFooMatchesExpectations(foo);
+ * }
+ *
+ * almost works, but not quite. This is because if the code under test calls foo() twice, the
+ * first call to checkFooCalled() matches both the calls, putting both calls into the captor,
+ * and then fails with TooManyActualInvocations. It also makes it harder to use other mockito
+ * features such as never(), inOrder(), etc.
+ *
+ * This approach isn't great because if the match fails, the error message is unhelpful
+ * (actual: "android.net.TetherOffloadRuleParcel@8c827b0" or some such), but at least it does
+ * work.
+ *
+ * TODO: consider making the error message more readable by adding a method that catching the
+ * AssertionFailedError and throwing a new assertion with more details. See
+ * NetworkMonitorTest#verifyNetworkTested.
+ *
+ * See ConnectivityServiceTest#assertRoutesAdded for an alternative approach which solves the
+ * TooManyActualInvocations problem described above by forcing the caller of the custom assert
+ * method to specify all expected invocations in one call. This is useful when the stable
+ * parcelable class being asserted on has a corresponding Java object (eg., RouteInfo and
+ * RouteInfoParcelable), and the caller can just pass in a list of them. It not useful here
+ * because there is no such object.
+ */
+ private static class TetherOffloadRuleParcelMatcher implements
+ ArgumentMatcher<TetherOffloadRuleParcel> {
+ public final int upstreamIfindex;
+ public final InetAddress dst;
+ public final MacAddress dstMac;
+
+ TetherOffloadRuleParcelMatcher(int upstreamIfindex, InetAddress dst, MacAddress dstMac) {
+ this.upstreamIfindex = upstreamIfindex;
+ this.dst = dst;
+ this.dstMac = dstMac;
+ }
+
+ public boolean matches(TetherOffloadRuleParcel parcel) {
+ return upstreamIfindex == parcel.inputInterfaceIndex
+ && (TEST_IFACE_PARAMS.index == parcel.outputInterfaceIndex)
+ && Arrays.equals(dst.getAddress(), parcel.destination)
+ && (128 == parcel.prefixLength)
+ && Arrays.equals(TEST_IFACE_PARAMS.macAddr.toByteArray(), parcel.srcL2Address)
+ && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address);
+ }
+
+ public String toString() {
+ return String.format("TetherOffloadRuleParcelMatcher(%d, %s, %s",
+ upstreamIfindex, dst.getHostAddress(), dstMac);
+ }
+ }
+
+ @NonNull
+ private static TetherOffloadRuleParcel matches(
+ int upstreamIfindex, InetAddress dst, MacAddress dstMac) {
+ return argThat(new TetherOffloadRuleParcelMatcher(upstreamIfindex, dst, dstMac));
+ }
+
+ @NonNull
+ private static Ipv6ForwardingRule makeForwardingRule(
+ int upstreamIfindex, @NonNull InetAddress dst, @NonNull MacAddress dstMac) {
+ return new Ipv6ForwardingRule(upstreamIfindex, TEST_IFACE_PARAMS.index,
+ (Inet6Address) dst, TEST_IFACE_PARAMS.macAddr, dstMac);
+ }
+
+ @NonNull
+ private static TetherDownstream6Key makeDownstream6Key(int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst) {
+ return new TetherDownstream6Key(upstreamIfindex, upstreamMac, dst.getAddress());
+ }
+
+ @NonNull
+ private static Tether6Value makeDownstream6Value(@NonNull final MacAddress dstMac) {
+ return new Tether6Value(TEST_IFACE_PARAMS.index, dstMac,
+ TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t);
+ } else {
+ return verify(t);
+ }
+ }
+
+ private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
+ @NonNull final MacAddress dstMac) throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
+ makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
+ makeDownstream6Value(dstMac));
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst,
+ dstMac));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
+ @NonNull final MacAddress dstMac) throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).updateEntry(
+ makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
+ makeDownstream6Value(dstMac));
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleAdd(any());
+ }
+ }
+
+ private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
+ @NonNull final MacAddress dstMac) throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(makeDownstream6Key(
+ upstreamIfindex, upstreamMac, dst));
+ } else {
+ // |dstMac| is not required for deleting rules. Used bacause tetherOffloadRuleRemove
+ // uses a whole rule to be a argument.
+ // See system/netd/server/TetherController.cpp/TetherController#removeOffloadRule.
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(upstreamIfindex, dst,
+ dstMac));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleRemove() throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).deleteEntry(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleRemove(any());
+ }
+ }
+
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex)
+ throws Exception {
+ if (!mBpfDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
+ ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ }
+
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder)
+ throws Exception {
+ if (!mBpfDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ }
+
+ private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
+ if (!mBpfDeps.isAtLeastS()) return;
+ if (inOrder != null) {
+ inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ }
+ }
+
+ @NonNull
+ private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
+ TetherStatsParcel parcel = new TetherStatsParcel();
+ parcel.ifIndex = ifIndex;
+ return parcel;
+ }
+
+ private void resetNetdBpfMapAndCoordinator() throws Exception {
+ reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfCoordinator);
+ // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+ // potentially crash the test) if the stats map is empty.
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
+ when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
+ .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
+ when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2))
+ .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2));
+ // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+ // potentially crash the test) if the stats map is empty.
+ final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0);
+ when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros);
+ when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros);
+ }
+
+ @Test
+ public void addRemoveipv6ForwardingRules() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
+ DEFAULT_USING_BPF_OFFLOAD);
+
+ final int myIfindex = TEST_IFACE_PARAMS.index;
+ final int notMyIfindex = myIfindex - 1;
+
+ final InetAddress neighA = InetAddresses.parseNumericAddress("2001:db8::1");
+ final InetAddress neighB = InetAddresses.parseNumericAddress("2001:db8::2");
+ final InetAddress neighLL = InetAddresses.parseNumericAddress("fe80::1");
+ final InetAddress neighMC = InetAddresses.parseNumericAddress("ff02::1234");
+ final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
+ final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
+ final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
+
+ resetNetdBpfMapAndCoordinator();
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
+ // tetherOffloadGetAndClearStats in netd while the rules are changed.
+
+ // Events on other interfaces are ignored.
+ recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // Events on this interface are received and sent to netd.
+ recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
+ verify(mBpfCoordinator).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ resetNetdBpfMapAndCoordinator();
+
+ recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
+ verify(mBpfCoordinator).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
+
+ // Link-local and multicast neighbors are ignored.
+ recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // A neighbor that is no longer valid causes the rule to be removed.
+ // NUD_FAILED events do not have a MAC address.
+ recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
+ verify(mBpfCoordinator).tetherOffloadRuleRemove(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull));
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
+
+ // A neighbor that is deleted causes the rule to be removed.
+ recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
+ verify(mBpfCoordinator).tetherOffloadRuleRemove(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull));
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
+ verifyStopUpstreamIpv6Forwarding(null);
+ resetNetdBpfMapAndCoordinator();
+
+ // Upstream changes result in updating the rules.
+ recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
+ resetNetdBpfMapAndCoordinator();
+
+ InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE2);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
+ verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(inOrder);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyNoUpstreamIpv6ForwardingChange(inOrder);
+ resetNetdBpfMapAndCoordinator();
+
+ // When the upstream is lost, rules are removed.
+ dispatchTetherConnectionChanged(null, null, 0);
+ // Clear function is called two times by:
+ // - 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)).tetherOffloadRuleClear(mIpServer);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(inOrder);
+ resetNetdBpfMapAndCoordinator();
+
+ // If the upstream is IPv4-only, no rules are added.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ resetNetdBpfMapAndCoordinator();
+ recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
+ // Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost.
+ verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // Rules can be added again once upstream IPv6 connectivity is available.
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
+ recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
+ verify(mBpfCoordinator).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
+ verifyNeverTetherOffloadRuleAdd(
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+
+ // If upstream IPv6 connectivity is lost, rules are removed.
+ resetNetdBpfMapAndCoordinator();
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
+ verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(null);
+
+ // When the interface goes down, rules are removed.
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
+ recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
+ recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
+ verify(mBpfCoordinator).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ verify(mBpfCoordinator).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ resetNetdBpfMapAndCoordinator();
+
+ mIpServer.stop();
+ mLooper.dispatchAll();
+ verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(null);
+ verify(mIpNeighborMonitor).stop();
+ resetNetdBpfMapAndCoordinator();
+ }
+
+ @Test
+ public void enableDisableUsingBpfOffload() throws Exception {
+ final int myIfindex = TEST_IFACE_PARAMS.index;
+ final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
+ final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
+ final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
+
+ // Expect that rules can be only added/removed when the BPF offload config is enabled.
+ // Note that the BPF offload disabled case is not a realistic test case. Because IP
+ // neighbor monitor doesn't start if BPF offload is disabled, there should have no
+ // neighbor event listening. This is used for testing the protection check just in case.
+ // TODO: Perhaps remove the BPF offload disabled case test once this check isn't needed
+ // anymore.
+
+ // [1] Enable BPF offload.
+ // A neighbor that is added or deleted causes the rule to be added or removed.
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
+ true /* usingBpfOffload */);
+ resetNetdBpfMapAndCoordinator();
+
+ recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
+ verify(mBpfCoordinator).tetherOffloadRuleAdd(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ resetNetdBpfMapAndCoordinator();
+
+ recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
+ verify(mBpfCoordinator).tetherOffloadRuleRemove(
+ mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull));
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull);
+ verifyStopUpstreamIpv6Forwarding(null);
+ resetNetdBpfMapAndCoordinator();
+
+ // [2] Disable BPF offload.
+ // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
+ false /* usingBpfOffload */);
+ resetNetdBpfMapAndCoordinator();
+
+ recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
+ verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any());
+ verifyNeverTetherOffloadRuleAdd();
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
+
+ recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
+ verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any());
+ verifyNeverTetherOffloadRuleRemove();
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
+ }
+
+ @Test
+ public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
+ false /* usingBpfOffload */);
+
+ // IP neighbor monitor doesn't start if BPF offload is disabled.
+ verify(mIpNeighborMonitor, never()).start();
+ }
+
+ private LinkProperties buildIpv6OnlyLinkProperties(final String iface) {
+ final LinkProperties linkProp = new LinkProperties();
+ linkProp.setInterfaceName(iface);
+ linkProp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+ linkProp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, iface, RTN_UNICAST));
+ final InetAddress dns = InetAddresses.parseNumericAddress("2001:4860:4860::8888");
+ linkProp.addDnsServer(dns);
+
+ return linkProp;
+ }
+
+ @Test
+ public void testAdjustTtlValue() throws Exception {
+ final ArgumentCaptor<RaParams> raParamsCaptor =
+ ArgumentCaptor.forClass(RaParams.class);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+ verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture());
+ final RaParams noV6Params = raParamsCaptor.getValue();
+ assertEquals(65, noV6Params.hopLimit);
+ reset(mRaDaemon);
+
+ when(mNetd.getProcSysNet(
+ INetd.IPV6, INetd.CONF, UPSTREAM_IFACE, "hop_limit")).thenReturn("64");
+ final LinkProperties lp = buildIpv6OnlyLinkProperties(UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 1);
+ verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture());
+ final RaParams nonCellularParams = raParamsCaptor.getValue();
+ assertEquals(65, nonCellularParams.hopLimit);
+ reset(mRaDaemon);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
+ verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture());
+ final RaParams noUpstream = raParamsCaptor.getValue();
+ assertEquals(65, nonCellularParams.hopLimit);
+ reset(mRaDaemon);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
+ verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture());
+ final RaParams cellularParams = raParamsCaptor.getValue();
+ assertEquals(63, cellularParams.hopLimit);
+ reset(mRaDaemon);
+ }
+
+ @Test
+ public void testStopObsoleteDhcpServer() throws Exception {
+ final ArgumentCaptor<DhcpServerCallbacks> cbCaptor =
+ ArgumentCaptor.forClass(DhcpServerCallbacks.class);
+ doNothing().when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(),
+ cbCaptor.capture());
+ initStateMachine(TETHERING_WIFI);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ verify(mDhcpServer, never()).startWithCallbacks(any(), any());
+
+ // No stop dhcp server because dhcp server is not created yet.
+ dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+ verify(mDhcpServer, never()).stop(any());
+
+ // Stop obsolete dhcp server.
+ try {
+ final DhcpServerCallbacks cb = cbCaptor.getValue();
+ cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ mLooper.dispatchAll();
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ verify(mDhcpServer).stop(any());
+ }
+
+ private void assertDhcpServingParams(final DhcpServingParamsParcel params,
+ final IpPrefix prefix) {
+ // Last address byte is random
+ assertTrue(prefix.contains(intToInet4AddressHTH(params.serverAddr)));
+ assertEquals(prefix.getPrefixLength(), params.serverAddrPrefixLength);
+ assertEquals(1, params.defaultRouters.length);
+ assertEquals(params.serverAddr, params.defaultRouters[0]);
+ assertEquals(1, params.dnsServers.length);
+ assertEquals(params.serverAddr, params.dnsServers[0]);
+ assertEquals(DHCP_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs);
+ if (mIpServer.interfaceType() == TETHERING_NCM) {
+ assertTrue(params.changePrefixOnDecline);
+ }
+ }
+
+ private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception {
+ verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any());
+ verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix);
+ }
+
+ /**
+ * Send a command to the state machine under test, and run the event loop to idle.
+ *
+ * @param command One of the IpServer.CMD_* constants.
+ * @param arg1 An additional argument to pass.
+ */
+ private void dispatchCommand(int command, int arg1) {
+ mIpServer.sendMessage(command, arg1);
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Send a command to the state machine under test, and run the event loop to idle.
+ *
+ * @param command One of the IpServer.CMD_* constants.
+ */
+ private void dispatchCommand(int command) {
+ mIpServer.sendMessage(command);
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Special override to tell the state machine that the upstream interface has changed.
+ *
+ * @see #dispatchCommand(int)
+ * @param upstreamIface String name of upstream interface (or null)
+ * @param v6lp IPv6 LinkProperties of the upstream interface, or null for an IPv4-only upstream.
+ */
+ private void dispatchTetherConnectionChanged(String upstreamIface, LinkProperties v6lp,
+ int ttlAdjustment) {
+ dispatchTetherConnectionChanged(upstreamIface);
+ mIpServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, ttlAdjustment, 0, v6lp);
+ mLooper.dispatchAll();
+ }
+
+ private void dispatchTetherConnectionChanged(String upstreamIface) {
+ final InterfaceSet ifs = (upstreamIface != null) ? new InterfaceSet(upstreamIface) : null;
+ mIpServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifs);
+ mLooper.dispatchAll();
+ }
+
+ private void assertIPv4AddressAndDirectlyConnectedRoute(LinkProperties lp) {
+ // Find the first IPv4 LinkAddress.
+ LinkAddress addr4 = null;
+ for (LinkAddress addr : lp.getLinkAddresses()) {
+ if (!(addr.getAddress() instanceof Inet4Address)) continue;
+ addr4 = addr;
+ break;
+ }
+ assertNotNull("missing IPv4 address", addr4);
+
+ final IpPrefix destination = new IpPrefix(addr4.getAddress(), addr4.getPrefixLength());
+ // Assert the presence of the associated directly connected route.
+ final RouteInfo directlyConnected = new RouteInfo(destination, null, lp.getInterfaceName(),
+ RouteInfo.RTN_UNICAST);
+ assertTrue("missing directly connected route: '" + directlyConnected.toString() + "'",
+ lp.getRoutes().contains(directlyConnected));
+ }
+
+ private void assertNoAddressesNorRoutes(LinkProperties lp) {
+ assertTrue(lp.getLinkAddresses().isEmpty());
+ assertTrue(lp.getRoutes().isEmpty());
+ // We also check that interface name is non-empty, because we should
+ // never see an empty interface name in any LinkProperties update.
+ assertFalse(TextUtils.isEmpty(lp.getInterfaceName()));
+ }
+
+ private boolean assertContainsFlag(String[] flags, String match) {
+ for (String flag : flags) {
+ if (flag.equals(match)) return true;
+ }
+ fail("Missing flag: " + match);
+ return false;
+ }
+
+ private boolean assertNotContainsFlag(String[] flags, String match) {
+ for (String flag : flags) {
+ if (flag.equals(match)) {
+ fail("Unexpected flag: " + match);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void dadProxyUpdates() throws Exception {
+ InOrder inOrder = inOrder(mDadProxy);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+ inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+ // Add an upstream without IPv6.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(null);
+
+ // Add IPv6 to the upstream.
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+ // Change upstream.
+ // New linkproperties is needed, otherwise changing the iface has no impact.
+ LinkProperties lp2 = new LinkProperties();
+ lp2.setInterfaceName(UPSTREAM_IFACE2);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS2);
+
+ // Lose IPv6 on the upstream...
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2, null, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(null);
+
+ // ... and regain it on a different upstream.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+ // Lose upstream.
+ dispatchTetherConnectionChanged(null, null, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(null);
+
+ // Regain upstream.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
+ inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+ // Stop tethering.
+ mIpServer.stop();
+ mLooper.dispatchAll();
+ }
+
+ private void checkDadProxyEnabled(boolean expectEnabled) throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+ InOrder inOrder = inOrder(mDadProxy);
+ // Add IPv6 to the upstream.
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ if (expectEnabled) {
+ inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+ } else {
+ inOrder.verifyNoMoreInteractions();
+ }
+ // Stop tethering.
+ mIpServer.stop();
+ mLooper.dispatchAll();
+ if (expectEnabled) {
+ inOrder.verify(mDadProxy).stop();
+ }
+ else {
+ verify(mDependencies, never()).getDadProxy(any(), any());
+ }
+ }
+ @Test @IgnoreAfter(Build.VERSION_CODES.R)
+ public void testDadProxyUpdates_DisabledUpToR() throws Exception {
+ checkDadProxyEnabled(false);
+ }
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testDadProxyUpdates_EnabledAfterR() throws Exception {
+ checkDadProxyEnabled(true);
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java b/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java
new file mode 100644
index 0000000..ea084b6
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceSetTest {
+ @Test
+ public void testNullNamesIgnored() {
+ final InterfaceSet set = new InterfaceSet(null, "if1", null, "if2", null);
+ assertEquals(2, set.ifnames.size());
+ assertTrue(set.ifnames.contains("if1"));
+ assertTrue(set.ifnames.contains("if2"));
+ }
+
+ @Test
+ public void testToString() {
+ final InterfaceSet set = new InterfaceSet("if1", "if2");
+ final String setString = set.toString();
+ assertTrue(setString.equals("[if1,if2]") || setString.equals("[if2,if1]"));
+ }
+
+ @Test
+ public void testToString_Empty() {
+ final InterfaceSet set = new InterfaceSet(null, null);
+ assertEquals("[]", set.toString());
+ }
+
+ @Test
+ public void testEquals() {
+ assertEquals(new InterfaceSet(null, "if1", "if2"), new InterfaceSet("if2", "if1"));
+ assertEquals(new InterfaceSet(null, null), new InterfaceSet());
+ assertFalse(new InterfaceSet("if1", "if3").equals(new InterfaceSet("if1", "if2")));
+ assertFalse(new InterfaceSet("if1", "if2").equals(new InterfaceSet("if1")));
+ assertFalse(new InterfaceSet().equals(null));
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
new file mode 100644
index 0000000..e5d0b1c
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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 android.net.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;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_DGRAM;
+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;
+import android.net.TetheringRequestParcel;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+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;
+
+import java.io.FileDescriptor;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TetheringUtilsTest {
+ private static final LinkAddress TEST_SERVER_ADDR = new LinkAddress("192.168.43.1/24");
+ 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;
+ request.localIPv4Address = TEST_SERVER_ADDR;
+ request.staticClientAddress = TEST_CLIENT_ADDR;
+ request.exemptFromEntitlementCheck = false;
+ request.showProvisioningUi = true;
+ 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.
+ private int writePacket(FileDescriptor fd, ByteBuffer pkt) throws Exception {
+ pkt.flip();
+ int offset = Struct.getSize(EthernetHeader.class);
+ int len = pkt.capacity() - offset;
+ return Os.write(fd, pkt.array(), offset, len);
+ }
+
+ // Reads a packet from the filedescriptor.
+ private ByteBuffer readIpPacket(FileDescriptor fd) throws Exception {
+ ByteBuffer buf = ByteBuffer.allocate(PACKET_SIZE);
+ Os.read(fd, buf);
+ return buf;
+ }
+
+ private interface SocketFilter {
+ void apply(FileDescriptor fd) throws Exception;
+ }
+
+ private ByteBuffer checkIcmpSocketFilter(ByteBuffer passed, ByteBuffer dropped,
+ SocketFilter filter) throws Exception {
+ FileDescriptor in = new FileDescriptor();
+ FileDescriptor out = new FileDescriptor();
+ Os.socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, in, out);
+
+ // Before the filter is applied, it doesn't drop anything.
+ int len = writePacket(out, dropped);
+ ByteBuffer received = readIpPacket(in);
+ assertEquals(len, received.position());
+
+ // Install the socket filter. Then write two packets, the first expected to be dropped and
+ // the second expected to be passed. Check that only the second makes it through.
+ filter.apply(in);
+ writePacket(out, dropped);
+ len = writePacket(out, passed);
+ received = readIpPacket(in);
+ assertEquals(len, received.position());
+ received.flip();
+
+ // Check there are no more packets to read.
+ try {
+ readIpPacket(in);
+ } catch (ErrnoException expected) {
+ assertEquals(EAGAIN, expected.errno);
+ }
+
+ return received;
+ }
+
+ @Test
+ public void testIcmpSocketFilters() throws Exception {
+ MacAddress mac1 = MacAddress.fromString("11:22:33:44:55:66");
+ MacAddress mac2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+ Inet6Address ll1 = (Inet6Address) InetAddress.getByName("fe80::1");
+ Inet6Address ll2 = (Inet6Address) InetAddress.getByName("fe80::abcd");
+ Inet6Address allRouters = NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+ final ByteBuffer na = Ipv6Utils.buildNaPacket(mac1, mac2, ll1, ll2, 0, ll1);
+ final ByteBuffer ns = Ipv6Utils.buildNsPacket(mac1, mac2, ll1, ll2, ll1);
+ final ByteBuffer rs = Ipv6Utils.buildRsPacket(mac1, mac2, ll1, allRouters);
+
+ ByteBuffer received = checkIcmpSocketFilter(na /* passed */, rs /* dropped */,
+ TetheringUtils::setupNaSocket);
+
+ Struct.parse(Ipv6Header.class, received); // Skip IPv6 header.
+ Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, received);
+ assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT, icmpv6.type);
+
+ received = checkIcmpSocketFilter(ns /* passed */, rs /* dropped */,
+ TetheringUtils::setupNsSocket);
+
+ Struct.parse(Ipv6Header.class, received); // Skip IPv6 header.
+ icmpv6 = Struct.parse(Icmpv6Header.class, received);
+ assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION, icmpv6.type);
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java
new file mode 100644
index 0000000..5a9b6e3
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.reset;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
+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;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VersionedBroadcastListenerTest {
+ private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
+ private static final String ACTION_TEST = "action.test.happy.broadcasts";
+
+ @Mock private Context mContext;
+ private BroadcastInterceptingContext mServiceContext;
+ private Handler mHandler;
+ private VersionedBroadcastListener mListener;
+ private int mCallbackCount;
+
+ private void doCallback() {
+ mCallbackCount++;
+ }
+
+ private class MockContext extends BroadcastInterceptingContext {
+ MockContext(Context base) {
+ super(base);
+ }
+ }
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ }
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ reset(mContext);
+ mServiceContext = new MockContext(mContext);
+ mHandler = new Handler(Looper.myLooper());
+ mCallbackCount = 0;
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_TEST);
+ mListener = new VersionedBroadcastListener(
+ TAG, mServiceContext, mHandler, filter, (Intent intent) -> doCallback());
+ }
+
+ @After public void tearDown() throws Exception {
+ if (mListener != null) {
+ mListener.stopListening();
+ mListener = null;
+ }
+ }
+
+ private void sendBroadcast() {
+ final Intent intent = new Intent(ACTION_TEST);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ @Test
+ public void testBasicListening() {
+ assertEquals(0, mCallbackCount);
+ mListener.startListening();
+ for (int i = 0; i < 5; i++) {
+ sendBroadcast();
+ assertEquals(i + 1, mCallbackCount);
+ }
+ mListener.stopListening();
+ }
+
+ @Test
+ public void testBroadcastsBeforeStartAreIgnored() {
+ assertEquals(0, mCallbackCount);
+ for (int i = 0; i < 5; i++) {
+ sendBroadcast();
+ assertEquals(0, mCallbackCount);
+ }
+
+ mListener.startListening();
+ sendBroadcast();
+ assertEquals(1, mCallbackCount);
+ }
+
+ @Test
+ public void testBroadcastsAfterStopAreIgnored() {
+ mListener.startListening();
+ sendBroadcast();
+ assertEquals(1, mCallbackCount);
+ mListener.stopListening();
+
+ for (int i = 0; i < 5; i++) {
+ sendBroadcast();
+ assertEquals(1, mCallbackCount);
+ }
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
new file mode 100644
index 0000000..cc912f4
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -0,0 +1,1497 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
+import static android.net.netlink.ConntrackMessage.DYING_MASK;
+import static android.net.netlink.ConntrackMessage.ESTABLISHED_MASK;
+import static android.net.netlink.ConntrackMessage.Tuple;
+import static android.net.netlink.ConntrackMessage.TupleIpv4;
+import static android.net.netlink.ConntrackMessage.TupleProto;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+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;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
+import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
+import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
+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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.NetworkStatsManager;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.NetworkStats;
+import android.net.TetherOffloadRuleParcel;
+import android.net.TetherStatsParcel;
+import android.net.ip.ConntrackMonitor;
+import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
+import android.net.ip.IpServer;
+import android.net.netlink.NetlinkConstants;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.Build;
+import android.os.Handler;
+import android.os.test.TestLooper;
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+import com.android.networkstack.tethering.BpfCoordinator.BpfConntrackEventConsumer;
+import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.TestableNetworkStatsProviderCbBinder;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BpfCoordinatorTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final int UPSTREAM_IFINDEX = 1001;
+ private static final int DOWNSTREAM_IFINDEX = 1002;
+
+ private static final String UPSTREAM_IFACE = "rmnet0";
+
+ private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
+ private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
+ private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
+
+ private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+
+ private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
+ UPSTREAM_IFACE, UPSTREAM_IFINDEX, null /* macAddr, rawip */,
+ NetworkStackConstants.ETHER_MTU);
+
+ // The test fake BPF map class is needed because the test has no privilege to access the BPF
+ // map. All member functions which eventually call JNI to access the real native BPF map need
+ // to be overridden.
+ // TODO: consider moving to an individual file.
+ private class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+ private final HashMap<K, V> mMap = new HashMap<K, V>();
+
+ TestBpfMap(final Class<K> key, final Class<V> value) {
+ super(key, value);
+ }
+
+ @Override
+ public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to
+ // implement the entry deletion in the iteration if required.
+ for (Map.Entry<K, V> entry : mMap.entrySet()) {
+ action.accept(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public void updateEntry(K key, V value) throws ErrnoException {
+ mMap.put(key, value);
+ }
+
+ @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.
+ if (mMap.get(key) != null) {
+ throw new IllegalArgumentException(key + " already exist");
+ }
+ mMap.put(key, value);
+ }
+
+ @Override
+ public boolean deleteEntry(Struct key) throws ErrnoException {
+ return mMap.remove(key) != null;
+ }
+
+ @Override
+ public V getValue(@NonNull K key) throws ErrnoException {
+ // Return value for a given key. Otherwise, return null without an error ENOENT.
+ // BpfMap#getValue treats that the entry is not found as no error.
+ return mMap.get(key);
+ }
+
+ @Override
+ public void clear() throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
+ mMap.clear();
+ }
+ };
+
+ @Mock private NetworkStatsManager mStatsManager;
+ @Mock private INetd mNetd;
+ @Mock private IpServer mIpServer;
+ @Mock private IpServer mIpServer2;
+ @Mock private TetheringConfiguration mTetherConfig;
+ @Mock private ConntrackMonitor mConntrackMonitor;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+ @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+ @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+ @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
+
+ // Late init since methods must be called by the thread that created this object.
+ private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
+ private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
+
+ // Late init since the object must be initialized by the BPF coordinator instance because
+ // it has to access the non-static function of BPF coordinator.
+ private BpfConntrackEventConsumer mConsumer;
+
+ private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
+ ArgumentCaptor.forClass(ArrayList.class);
+ private final TestLooper mTestLooper = new TestLooper();
+ private final TestBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap =
+ spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class));
+ private final TestBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap =
+ spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class));
+ private BpfCoordinator.Dependencies mDeps =
+ spy(new BpfCoordinator.Dependencies() {
+ @NonNull
+ public Handler getHandler() {
+ return new Handler(mTestLooper.getLooper());
+ }
+
+ @NonNull
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ @NonNull
+ public NetworkStatsManager getNetworkStatsManager() {
+ return mStatsManager;
+ }
+
+ @NonNull
+ public SharedLog getSharedLog() {
+ return new SharedLog("test");
+ }
+
+ @Nullable
+ public TetheringConfiguration getTetherConfig() {
+ return mTetherConfig;
+ }
+
+ @NonNull
+ public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) {
+ return mConntrackMonitor;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+ return mBpfDownstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+ return mBpfUpstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+ return mBpfDownstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+ return mBpfUpstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ return mBpfStatsMap;
+ }
+
+ @Nullable
+ public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+ return mBpfLimitMap;
+ }
+
+ @Nullable
+ public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+ return mBpfDevMap;
+ }
+ });
+
+ @Before public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
+ }
+
+ private void waitForIdle() {
+ mTestLooper.dispatchAll();
+ }
+
+ // TODO: Remove unnecessary calling on R because the BPF map accessing has been moved into
+ // module.
+ private void setupFunctioningNetdInterface() throws Exception {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
+ }
+
+ @NonNull
+ private BpfCoordinator makeBpfCoordinator() throws Exception {
+ final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
+
+ mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
+ final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
+ tetherStatsProviderCaptor =
+ ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
+ verify(mStatsManager).registerNetworkStatsProvider(anyString(),
+ tetherStatsProviderCaptor.capture());
+ mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
+ assertNotNull(mTetherStatsProvider);
+ mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
+ mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
+
+ return coordinator;
+ }
+
+ @NonNull
+ private static NetworkStats.Entry buildTestEntry(@NonNull StatsType how,
+ @NonNull String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+ return new NetworkStats.Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING,
+ SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes,
+ rxPackets, txBytes, txPackets, 0L);
+ }
+
+ @NonNull
+ private static TetherStatsParcel buildTestTetherStatsParcel(@NonNull Integer ifIndex,
+ long rxBytes, long rxPackets, long txBytes, long txPackets) {
+ final TetherStatsParcel parcel = new TetherStatsParcel();
+ parcel.ifIndex = ifIndex;
+ parcel.rxBytes = rxBytes;
+ parcel.rxPackets = rxPackets;
+ parcel.txBytes = txBytes;
+ parcel.txPackets = txPackets;
+ return parcel;
+ }
+
+ // Update a stats entry or create if not exists.
+ private void updateStatsEntryToStatsMap(@NonNull TetherStatsParcel stats) throws Exception {
+ final TetherStatsKey key = new TetherStatsKey(stats.ifIndex);
+ final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes,
+ 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */);
+ mBpfStatsMap.updateEntry(key, value);
+ }
+
+ private void updateStatsEntry(@NonNull TetherStatsParcel stats) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ updateStatsEntryToStatsMap(stats);
+ } else {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[] {stats});
+ }
+ }
+
+ // Update specific tether stats list and wait for the stats cache is updated by polling thread
+ // in the coordinator. Beware of that it is only used for the default polling interval.
+ // Note that the mocked tetherOffloadGetStats of netd replaces all stats entries because it
+ // doesn't store the previous entries.
+ private void updateStatsEntriesAndWaitForUpdate(@NonNull TetherStatsParcel[] tetherStatsList)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ for (TetherStatsParcel stats : tetherStatsList) {
+ updateStatsEntry(stats);
+ }
+ } else {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
+ }
+
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ }
+
+ // In tests, the stats need to be set before deleting the last rule.
+ // The reason is that BpfCoordinator#tetherOffloadRuleRemove reads the stats
+ // of the deleting interface after the last rule deleted. #tetherOffloadRuleRemove
+ // does the interface cleanup failed if there is no stats for the deleting interface.
+ // Note that the mocked tetherOffloadGetAndClearStats of netd replaces all stats entries
+ // because it doesn't store the previous entries.
+ private void updateStatsEntryForTetherOffloadGetAndClearStats(TetherStatsParcel stats)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ updateStatsEntryToStatsMap(stats);
+ } else {
+ when(mNetd.tetherOffloadGetAndClearStats(stats.ifIndex)).thenReturn(stats);
+ }
+ }
+
+ private void clearStatsInvocations() {
+ if (mDeps.isAtLeastS()) {
+ clearInvocations(mBpfStatsMap);
+ } else {
+ clearInvocations(mNetd);
+ }
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t);
+ } else {
+ return verify(t);
+ }
+ }
+
+ private void verifyTetherOffloadGetStats() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfStatsMap).forEach(any());
+ } else {
+ verify(mNetd).tetherOffloadGetStats();
+ }
+ }
+
+ private void verifyNeverTetherOffloadGetStats() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfStatsMap, never()).forEach(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadGetStats();
+ }
+ }
+
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
+ MacAddress downstreamMac, int upstreamIfindex) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
+ ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ }
+
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
+ MacAddress downstreamMac)
+ throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ }
+
+ private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ if (inOrder != null) {
+ inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ }
+ }
+
+ private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder,
+ @NonNull Ipv6ForwardingRule rule) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
+ rule.makeTetherDownstream6Key(), rule.makeTether6Value());
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(rule));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleAdd(any());
+ }
+ }
+
+ private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder,
+ @NonNull final Ipv6ForwardingRule rule) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(
+ rule.makeTetherDownstream6Key());
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(rule));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleRemove() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).deleteEntry(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleRemove(any());
+ }
+ }
+
+ private void verifyTetherOffloadSetInterfaceQuota(@Nullable InOrder inOrder, int ifIndex,
+ long quotaBytes, boolean isInit) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ final TetherStatsKey key = new TetherStatsKey(ifIndex);
+ verifyWithOrder(inOrder, mBpfStatsMap).getValue(key);
+ if (isInit) {
+ verifyWithOrder(inOrder, mBpfStatsMap).insertEntry(key, new TetherStatsValue(
+ 0L /* rxPackets */, 0L /* rxBytes */, 0L /* rxErrors */,
+ 0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */));
+ }
+ verifyWithOrder(inOrder, mBpfLimitMap).updateEntry(new TetherLimitKey(ifIndex),
+ new TetherLimitValue(quotaBytes));
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
+ }
+ }
+
+ private void verifyNeverTetherOffloadSetInterfaceQuota(@NonNull InOrder inOrder)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ inOrder.verify(mBpfStatsMap, never()).getValue(any());
+ inOrder.verify(mBpfStatsMap, never()).insertEntry(any(), any());
+ inOrder.verify(mBpfLimitMap, never()).updateEntry(any(), any());
+ } else {
+ inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+ }
+ }
+
+ private void verifyTetherOffloadGetAndClearStats(@NonNull InOrder inOrder, int ifIndex)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ inOrder.verify(mBpfStatsMap).getValue(new TetherStatsKey(ifIndex));
+ inOrder.verify(mBpfStatsMap).deleteEntry(new TetherStatsKey(ifIndex));
+ inOrder.verify(mBpfLimitMap).deleteEntry(new TetherLimitKey(ifIndex));
+ } else {
+ inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ifIndex);
+ }
+ }
+
+ // S+ and R api minimum tests.
+ // The following tests are used to provide minimum checking for the APIs on different flow.
+ // The auto merge is not enabled on mainline prod. The code flow R may be verified at the
+ // late stage by manual cherry pick. It is risky if the R code flow has broken and be found at
+ // the last minute.
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ private void checkTetherOffloadRuleAddAndRemove(boolean usingApiS) throws Exception {
+ setupFunctioningNetdInterface();
+
+ // Replace Dependencies#isAtLeastS() for testing R and S+ BPF map apis. Note that |mDeps|
+ // must be mocked before calling #makeBpfCoordinator which use |mDeps| to initialize the
+ // coordinator.
+ doReturn(usingApiS).when(mDeps).isAtLeastS();
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // InOrder is required because mBpfStatsMap may be accessed by both
+ // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
+ // The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called
+ // mBpfStatsMap#getValue and get a wrong calling count which counts all.
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+ coordinator.tetherOffloadRuleAdd(mIpServer, rule);
+ verifyTetherOffloadRuleAdd(inOrder, rule);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+
+ // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
+ coordinator.tetherOffloadRuleRemove(mIpServer, rule);
+ verifyTetherOffloadRuleRemove(inOrder, rule);
+ verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadRuleAddAndRemoveSdkR() throws Exception {
+ checkTetherOffloadRuleAddAndRemove(false /* R */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadRuleAddAndRemoveAtLeastSdkS() throws Exception {
+ checkTetherOffloadRuleAddAndRemove(true /* S+ */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ private void checkTetherOffloadGetStats(boolean usingApiS) throws Exception {
+ setupFunctioningNetdInterface();
+
+ doReturn(usingApiS).when(mDeps).isAtLeastS();
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)});
+
+ final NetworkStats expectedIfaceStats = new NetworkStats(0L, 1)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 1000, 100, 2000, 200));
+
+ final NetworkStats expectedUidStats = new NetworkStats(0L, 1)
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 1000, 100, 2000, 200));
+
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadGetStatsSdkR() throws Exception {
+ checkTetherOffloadGetStats(false /* R */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadGetStatsAtLeastSdkS() throws Exception {
+ checkTetherOffloadGetStats(true /* S+ */);
+ }
+
+ @Test
+ public void testGetForwardedStats() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+
+ final String wlanIface = "wlan0";
+ final Integer wlanIfIndex = 100;
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 101;
+
+ // Add interface name to lookup table. In realistic case, the upstream interface name will
+ // be added by IpServer when IpServer has received with a new IPv6 upstream update event.
+ coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // [1] Both interface stats are changed.
+ // Setup the tether stats of wlan and mobile interface. Note that move forward the time of
+ // the looper to make sure the new tether stats has been updated by polling update thread.
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
+ buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)});
+
+ final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 1000, 100, 2000, 200))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 3000, 300, 4000, 400));
+
+ final NetworkStats expectedUidStats = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 1000, 100, 2000, 200))
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 3000, 300, 4000, 400));
+
+ // Force pushing stats update to verify the stats reported.
+ // TODO: Perhaps make #expectNotifyStatsUpdated to use test TetherStatsParcel object for
+ // verifying the notification.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
+
+ // [2] Only one interface stats is changed.
+ // The tether stats of mobile interface is accumulated and The tether stats of wlan
+ // interface is the same.
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
+ buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)});
+
+ final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 0, 0, 0, 0))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 10, 20, 30, 40));
+
+ final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 0, 0, 0, 0))
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 10, 20, 30, 40));
+
+ // Force pushing stats update to verify that only diff of stats is reported.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
+ expectedUidStatsDiff);
+
+ // [3] Stop coordinator.
+ // Shutdown the coordinator and clear the invocation history, especially the
+ // tetherOffloadGetStats() calls.
+ coordinator.stopPolling();
+ clearStatsInvocations();
+
+ // Verify the polling update thread stopped.
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ verifyNeverTetherOffloadGetStats();
+ }
+
+ @Test
+ public void testOnSetAlert() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // Verify that set quota to 0 will immediately triggers a callback.
+ mTetherStatsProvider.onSetAlert(0);
+ waitForIdle();
+ mTetherStatsProviderCb.expectNotifyAlertReached();
+
+ // Verify that notifyAlertReached never fired if quota is not yet reached.
+ updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
+ mTetherStatsProvider.onSetAlert(100);
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ mTetherStatsProviderCb.assertNoCallback();
+
+ // Verify that notifyAlertReached fired when quota is reached.
+ updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0));
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ mTetherStatsProviderCb.expectNotifyAlertReached();
+
+ // Verify that set quota with UNLIMITED won't trigger any callback.
+ mTetherStatsProvider.onSetAlert(QUOTA_UNLIMITED);
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ mTetherStatsProviderCb.assertNoCallback();
+ }
+
+ // The custom ArgumentMatcher simply comes from IpServerTest.
+ // TODO: move both of them into a common utility class for reusing the code.
+ private static class TetherOffloadRuleParcelMatcher implements
+ ArgumentMatcher<TetherOffloadRuleParcel> {
+ public final int upstreamIfindex;
+ public final int downstreamIfindex;
+ public final Inet6Address address;
+ public final MacAddress srcMac;
+ public final MacAddress dstMac;
+
+ TetherOffloadRuleParcelMatcher(@NonNull Ipv6ForwardingRule rule) {
+ upstreamIfindex = rule.upstreamIfindex;
+ downstreamIfindex = rule.downstreamIfindex;
+ address = rule.address;
+ srcMac = rule.srcMac;
+ dstMac = rule.dstMac;
+ }
+
+ public boolean matches(@NonNull TetherOffloadRuleParcel parcel) {
+ return upstreamIfindex == parcel.inputInterfaceIndex
+ && (downstreamIfindex == parcel.outputInterfaceIndex)
+ && Arrays.equals(address.getAddress(), parcel.destination)
+ && (128 == parcel.prefixLength)
+ && Arrays.equals(srcMac.toByteArray(), parcel.srcL2Address)
+ && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address);
+ }
+
+ public String toString() {
+ return String.format("TetherOffloadRuleParcelMatcher(%d, %d, %s, %s, %s",
+ upstreamIfindex, downstreamIfindex, address.getHostAddress(), srcMac, dstMac);
+ }
+ }
+
+ @NonNull
+ private TetherOffloadRuleParcel matches(@NonNull Ipv6ForwardingRule rule) {
+ return argThat(new TetherOffloadRuleParcelMatcher(rule));
+ }
+
+ @NonNull
+ private static Ipv6ForwardingRule buildTestForwardingRule(
+ int upstreamIfindex, @NonNull InetAddress address, @NonNull MacAddress dstMac) {
+ return new Ipv6ForwardingRule(upstreamIfindex, DOWNSTREAM_IFINDEX, (Inet6Address) address,
+ DOWNSTREAM_MAC, dstMac);
+ }
+
+ @Test
+ public void testRuleMakeTetherDownstream6Key() throws Exception {
+ final Integer mobileIfIndex = 100;
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+
+ final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
+ assertEquals(key.iif, (long) mobileIfIndex);
+ assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS); // rawip upstream
+ assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
+ // iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28.
+ assertEquals(28, key.writeToBytes().length);
+ }
+
+ @Test
+ public void testRuleMakeTether6Value() throws Exception {
+ final Integer mobileIfIndex = 100;
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+
+ final Tether6Value value = rule.makeTether6Value();
+ assertEquals(value.oif, DOWNSTREAM_IFINDEX);
+ assertEquals(value.ethDstMac, MAC_A);
+ assertEquals(value.ethSrcMac, DOWNSTREAM_MAC);
+ assertEquals(value.ethProto, ETH_P_IPV6);
+ assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU);
+ // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20.
+ assertEquals(20, value.writeToBytes().length);
+ }
+
+ @Test
+ public void testSetDataLimit() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // [1] Default limit.
+ // Set the unlimited quota as default if the service has never applied a data limit for a
+ // given upstream. Note that the data limit only be applied on an upstream which has rules.
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+ coordinator.tetherOffloadRuleAdd(mIpServer, rule);
+ verifyTetherOffloadRuleAdd(inOrder, rule);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Specific limit.
+ // Applying the data limit boundary {min, 1gb, max, infinity} on current upstream.
+ for (final long quota : new long[] {0, 1048576000, Long.MAX_VALUE, QUOTA_UNLIMITED}) {
+ mTetherStatsProvider.onSetLimit(mobileIface, quota);
+ waitForIdle();
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, quota,
+ false /* isInit */);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ // [3] Invalid limit.
+ // The valid range of quota is 0..max_int64 or -1 (unlimited).
+ final long invalidLimit = Long.MIN_VALUE;
+ try {
+ mTetherStatsProvider.onSetLimit(mobileIface, invalidLimit);
+ waitForIdle();
+ fail("No exception thrown for invalid limit " + invalidLimit + ".");
+ } catch (IllegalArgumentException expected) {
+ assertEquals(expected.getMessage(), "invalid quota value " + invalidLimit);
+ }
+ }
+
+ // TODO: Test the case in which the rules are changed from different IpServer objects.
+ @Test
+ public void testSetDataLimitOnRule6Change() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(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.
+ final long limit = 12345;
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+ mTetherStatsProvider.onSetLimit(mobileIface, limit);
+ waitForIdle();
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+
+ // Adding the first rule on current upstream immediately sends the quota to netd.
+ final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+ coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
+ verifyTetherOffloadRuleAdd(inOrder, ruleA);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
+ inOrder.verifyNoMoreInteractions();
+
+ // Adding the second rule on current upstream does not send the quota to netd.
+ final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
+ coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
+ verifyTetherOffloadRuleAdd(inOrder, ruleB);
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+
+ // Removing the second rule on current upstream does not send the quota to netd.
+ coordinator.tetherOffloadRuleRemove(mIpServer, ruleB);
+ verifyTetherOffloadRuleRemove(inOrder, ruleB);
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+
+ // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
+ coordinator.tetherOffloadRuleRemove(mIpServer, ruleA);
+ verifyTetherOffloadRuleRemove(inOrder, ruleA);
+ verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testTetherOffloadRuleUpdateAndClear() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String ethIface = "eth1";
+ final String mobileIface = "rmnet_data0";
+ final Integer ethIfIndex = 100;
+ final Integer mobileIfIndex = 101;
+ coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
+ mBpfStatsMap);
+
+ // Before the rule test, here are the additional actions while the rules are changed.
+ // - After adding the first rule on a given upstream, the coordinator adds a data limit.
+ // If the service has never applied the data limit, set an unlimited quota as default.
+ // - After removing the last rule on a given upstream, the coordinator gets the last stats.
+ // Then, it clears the stats and the limit entry from BPF maps.
+ // See tetherOffloadRule{Add, Remove, Clear, Clean}.
+
+ // [1] Adding rules on the upstream Ethernet.
+ // Note that the default data limit is applied after the first rule is added.
+ final Ipv6ForwardingRule ethernetRuleA = buildTestForwardingRule(
+ ethIfIndex, NEIGH_A, MAC_A);
+ final Ipv6ForwardingRule ethernetRuleB = buildTestForwardingRule(
+ ethIfIndex, NEIGH_B, MAC_B);
+
+ coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
+ verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+ verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex);
+ coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
+ verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
+
+ // [2] Update the existing rules from Ethernet to cellular.
+ final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule(
+ mobileIfIndex, NEIGH_A, MAC_A);
+ final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule(
+ mobileIfIndex, NEIGH_B, MAC_B);
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
+
+ // Update the existing rules for upstream changes. The rules are removed and re-added one
+ // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
+ coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
+ verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA);
+ verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
+ verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+ verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ mobileIfIndex);
+ verifyTetherOffloadRuleAdd(inOrder, mobileRuleB);
+
+ // [3] Clear all rules for a given IpServer.
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
+ coordinator.tetherOffloadRuleClear(mIpServer);
+ verifyTetherOffloadRuleRemove(inOrder, mobileRuleA);
+ verifyTetherOffloadRuleRemove(inOrder, mobileRuleB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
+
+ // [4] Force pushing stats update to verify that the last diff of stats is reported on all
+ // upstreams.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(
+ new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, ethIface, 10, 20, 30, 40))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 50, 60, 70, 80)),
+ new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, ethIface, 10, 20, 30, 40))
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 50, 60, 70, 80)));
+ }
+
+ private void checkBpfDisabled() throws Exception {
+ // The caller may mock the global dependencies |mDeps| which is used in
+ // #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);
+ waitForIdle();
+ verifyNeverTetherOffloadGetStats();
+
+ // The interface name lookup table can't be added.
+ final String iface = "rmnet_data0";
+ final Integer ifIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(ifIndex, iface);
+ assertEquals(0, coordinator.getInterfaceNamesForTesting().size());
+
+ // The rule can't be added.
+ final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
+ final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac);
+ coordinator.tetherOffloadRuleAdd(mIpServer, rule);
+ verifyNeverTetherOffloadRuleAdd();
+ LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules =
+ coordinator.getForwardingRulesForTesting().get(mIpServer);
+ assertNull(rules);
+
+ // The rule can't be removed. This is not a realistic case because adding rule is not
+ // allowed. That implies no rule could be removed, cleared or updated. Verify these
+ // cases just in case.
+ rules = new LinkedHashMap<Inet6Address, Ipv6ForwardingRule>();
+ rules.put(rule.address, rule);
+ coordinator.getForwardingRulesForTesting().put(mIpServer, rules);
+ coordinator.tetherOffloadRuleRemove(mIpServer, rule);
+ verifyNeverTetherOffloadRuleRemove();
+ rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+ assertNotNull(rules);
+ assertEquals(1, rules.size());
+
+ // The rule can't be cleared.
+ coordinator.tetherOffloadRuleClear(mIpServer);
+ verifyNeverTetherOffloadRuleRemove();
+ rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+ assertNotNull(rules);
+ assertEquals(1, rules.size());
+
+ // The rule can't be updated.
+ coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
+ verifyNeverTetherOffloadRuleRemove();
+ verifyNeverTetherOffloadRuleAdd();
+ rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+ assertNotNull(rules);
+ assertEquals(1, rules.size());
+ }
+
+ @Test
+ public void testBpfDisabledbyConfig() throws Exception {
+ setupFunctioningNetdInterface();
+ when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfDownstream6Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfDownstream6Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfUpstream6Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfUpstream6Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfDownstream4Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfDownstream4Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfUpstream4Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfUpstream4Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfStatsMap() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfStatsMap();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfLimitMap() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfLimitMap();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfMapClear() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ verify(mBpfDownstream4Map).clear();
+ verify(mBpfUpstream4Map).clear();
+ verify(mBpfDownstream6Map).clear();
+ verify(mBpfUpstream6Map).clear();
+ verify(mBpfStatsMap).clear();
+ verify(mBpfLimitMap).clear();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testAttachDetachBpfProgram() throws Exception {
+ setupFunctioningNetdInterface();
+
+ // Static mocking for BpfUtils.
+ MockitoSession mockSession = ExtendedMockito.mockitoSession()
+ .mockStatic(BpfUtils.class)
+ .startMocking();
+ try {
+ final String intIface1 = "wlan1";
+ final String intIface2 = "rndis0";
+ final String extIface = "rmnet_data0";
+ final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class);
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ // [1] Add the forwarding pair <wlan1, rmnet_data0>. Expect that attach both wlan1 and
+ // rmnet_data0.
+ coordinator.maybeAttachProgram(intIface1, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM));
+ ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [2] Add the forwarding pair <wlan1, rmnet_data0> again. Expect no more action.
+ coordinator.maybeAttachProgram(intIface1, extIface);
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [3] Add the forwarding pair <rndis0, rmnet_data0>. Expect that attach rndis0 only.
+ coordinator.maybeAttachProgram(intIface2, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [4] Remove the forwarding pair <rndis0, rmnet_data0>. Expect detach rndis0 only.
+ coordinator.maybeDetachProgram(intIface2, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [5] Remove the forwarding pair <wlan1, rmnet_data0>. Expect that detach both wlan1
+ // and rmnet_data0.
+ coordinator.maybeDetachProgram(intIface1, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface));
+ ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+ } finally {
+ mockSession.finishMocking();
+ }
+ }
+
+ @Test
+ public void testTetheringConfigSetPollingInterval() throws Exception {
+ setupFunctioningNetdInterface();
+
+ 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.
+ // Use a large polling interval to avoid flaky test because the time forwarding
+ // 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());
+
+ // Start on a new polling time slot.
+ mTestLooper.moveTimeForward(pollingInterval);
+ waitForIdle();
+ clearStatsInvocations();
+
+ // Move time forward to 90% polling interval time. Expect that the polling thread has not
+ // scheduled yet.
+ mTestLooper.moveTimeForward((long) (pollingInterval * 0.9));
+ waitForIdle();
+ verifyNeverTetherOffloadGetStats();
+
+ // Move time forward to the remaining 10% polling interval time. Expect that the polling
+ // thread has scheduled.
+ mTestLooper.moveTimeForward((long) (pollingInterval * 0.1));
+ waitForIdle();
+ verifyTetherOffloadGetStats();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testStartStopConntrackMonitoring() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ // [1] Don't stop monitoring if it has never started.
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor, never()).start();
+
+ // [2] Start monitoring.
+ coordinator.startMonitoring(mIpServer);
+ verify(mConntrackMonitor).start();
+ clearInvocations(mConntrackMonitor);
+
+ // [3] Stop monitoring.
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor).stop();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @IgnoreAfter(Build.VERSION_CODES.R)
+ // Only run this test on Android R.
+ public void testStartStopConntrackMonitoring_R() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ coordinator.startMonitoring(mIpServer);
+ verify(mConntrackMonitor, never()).start();
+
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor, never()).stop();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ // [1] Start monitoring at the first IpServer adding.
+ coordinator.startMonitoring(mIpServer);
+ verify(mConntrackMonitor).start();
+ clearInvocations(mConntrackMonitor);
+
+ // [2] Don't start monitoring at the second IpServer adding.
+ coordinator.startMonitoring(mIpServer2);
+ verify(mConntrackMonitor, never()).start();
+
+ // [3] Don't stop monitoring if any downstream interface exists.
+ coordinator.stopMonitoring(mIpServer2);
+ verify(mConntrackMonitor, never()).stop();
+
+ // [4] Stop monitoring if no downstream exists.
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor).stop();
+ }
+
+ // Test network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ // remote ip public ip private ip
+ // 140.112.8.116:443 100.81.179.1:62449 192.168.80.12:62449
+ //
+ private static final Inet4Address REMOTE_ADDR =
+ (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+ private static final Inet4Address PUBLIC_ADDR =
+ (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+ private static final Inet4Address PRIVATE_ADDR =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+
+ // IPv4-mapped IPv6 addresses
+ // Remote addrress ::ffff:140.112.8.116
+ // Public addrress ::ffff:100.81.179.1
+ // Private addrress ::ffff:192.168.80.12
+ private static final byte[] REMOTE_ADDR_V4MAPPED_BYTES = new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0x8c, (byte) 0x70, (byte) 0x08, (byte) 0x74 };
+ private static final byte[] PUBLIC_ADDR_V4MAPPED_BYTES = new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0x64, (byte) 0x51, (byte) 0xb3, (byte) 0x01 };
+ private static final byte[] PRIVATE_ADDR_V4MAPPED_BYTES = new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0xa8, (byte) 0x50, (byte) 0x0c };
+
+ // Generally, public port and private port are the same in the NAT conntrack message.
+ // TODO: consider using different private port and public port for testing.
+ private static final short REMOTE_PORT = (short) 443;
+ private static final short PUBLIC_PORT = (short) 62449;
+ private static final short PRIVATE_PORT = (short) 62449;
+
+ @NonNull
+ private Tether4Key makeUpstream4Key(int proto) {
+ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+ fail("Not support protocol " + proto);
+ }
+ return new Tether4Key(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, (short) proto,
+ PRIVATE_ADDR.getAddress(), REMOTE_ADDR.getAddress(), PRIVATE_PORT, REMOTE_PORT);
+ }
+
+ @NonNull
+ private Tether4Key makeDownstream4Key(int proto) {
+ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+ fail("Not support protocol " + proto);
+ }
+ return new Tether4Key(UPSTREAM_IFINDEX,
+ MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, (short) proto,
+ REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), REMOTE_PORT, PUBLIC_PORT);
+ }
+
+ @NonNull
+ private Tether4Value makeUpstream4Value() {
+ return new Tether4Value(UPSTREAM_IFINDEX,
+ MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */,
+ MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
+ NetworkStackConstants.ETHER_MTU, PUBLIC_ADDR_V4MAPPED_BYTES,
+ REMOTE_ADDR_V4MAPPED_BYTES, PUBLIC_PORT, REMOTE_PORT, 0 /* lastUsed */);
+ }
+
+ @NonNull
+ private Tether4Value makeDownstream4Value() {
+ return new Tether4Value(DOWNSTREAM_IFINDEX, MAC_A /* client mac */, DOWNSTREAM_MAC,
+ ETH_P_IP, NetworkStackConstants.ETHER_MTU, REMOTE_ADDR_V4MAPPED_BYTES,
+ PRIVATE_ADDR_V4MAPPED_BYTES, REMOTE_PORT, PRIVATE_PORT, 0 /* lastUsed */);
+ }
+
+ @NonNull
+ private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) {
+ if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
+ fail("Not support message type " + msgType);
+ }
+ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+ fail("Not support protocol " + proto);
+ }
+
+ final int status = (msgType == IPCTNL_MSG_CT_NEW) ? ESTABLISHED_MASK : DYING_MASK;
+ final int timeoutSec = (msgType == IPCTNL_MSG_CT_NEW) ? 100 /* nonzero, new */
+ : 0 /* unused, delete */;
+ return new ConntrackEvent(
+ (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+ new Tuple(new TupleIpv4(PRIVATE_ADDR, REMOTE_ADDR),
+ new TupleProto((byte) proto, PRIVATE_PORT, REMOTE_PORT)),
+ new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR),
+ new TupleProto((byte) proto, REMOTE_PORT, PUBLIC_PORT)),
+ status,
+ timeoutSec);
+ }
+
+ private void setUpstreamInformationTo(final BpfCoordinator coordinator) {
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */));
+ coordinator.addUpstreamIfindexToMap(lp);
+ }
+
+ private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) {
+ final ClientInfo clientInfo = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR, MAC_A /* client mac */);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientInfo);
+ }
+
+ private void initBpfCoordinatorForRule4(final BpfCoordinator coordinator) throws Exception {
+ // Needed because addUpstreamIfindexToMap only updates upstream information when polling
+ // was started.
+ coordinator.startPolling();
+
+ // Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases
+ // the count while the entry is deleted. In the other words, deleteEntry returns true.
+ doReturn(true).when(mBpfDownstream4Map).deleteEntry(any());
+
+ // Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for
+ // interface index.
+ doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
+
+ coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+ setUpstreamInformationTo(coordinator);
+ setDownstreamAndClientInformationTo(coordinator);
+ }
+
+ // TODO: Test the IPv4 and IPv6 exist concurrently.
+ // TODO: Test the IPv4 rule delete failed.
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testSetDataLimitOnRule4Change() throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+
+ // 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.
+ final long limit = 12345;
+ final InOrder inOrder = inOrder(mNetd, mBpfUpstream4Map, mBpfDownstream4Map, mBpfLimitMap,
+ mBpfStatsMap);
+ mTetherStatsProvider.onSetLimit(UPSTREAM_IFACE, limit);
+ waitForIdle();
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+
+ // Build TCP and UDP rules for testing. Note that the values of {TCP, UDP} are the same
+ // because the protocol is not an element of the value. Consider using different address
+ // or port to make them different for better testing.
+ // TODO: Make the values of {TCP, UDP} rules different.
+ final Tether4Key expectedUpstream4KeyTcp = makeUpstream4Key(IPPROTO_TCP);
+ final Tether4Key expectedDownstream4KeyTcp = makeDownstream4Key(IPPROTO_TCP);
+ final Tether4Value expectedUpstream4ValueTcp = makeUpstream4Value();
+ final Tether4Value expectedDownstream4ValueTcp = makeDownstream4Value();
+
+ final Tether4Key expectedUpstream4KeyUdp = makeUpstream4Key(IPPROTO_UDP);
+ final Tether4Key expectedDownstream4KeyUdp = makeDownstream4Key(IPPROTO_UDP);
+ final Tether4Value expectedUpstream4ValueUdp = makeUpstream4Value();
+ final Tether4Value expectedDownstream4ValueUdp = makeDownstream4Value();
+
+ // [1] Adding the first rule on current upstream immediately sends the quota.
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
+ verifyTetherOffloadSetInterfaceQuota(inOrder, UPSTREAM_IFINDEX, limit, true /* isInit */);
+ inOrder.verify(mBpfUpstream4Map)
+ .insertEntry(eq(expectedUpstream4KeyTcp), eq(expectedUpstream4ValueTcp));
+ inOrder.verify(mBpfDownstream4Map)
+ .insertEntry(eq(expectedDownstream4KeyTcp), eq(expectedDownstream4ValueTcp));
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Adding the second rule on current upstream does not send the quota.
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+ inOrder.verify(mBpfUpstream4Map)
+ .insertEntry(eq(expectedUpstream4KeyUdp), eq(expectedUpstream4ValueUdp));
+ inOrder.verify(mBpfDownstream4Map)
+ .insertEntry(eq(expectedDownstream4KeyUdp), eq(expectedDownstream4ValueUdp));
+ inOrder.verifyNoMoreInteractions();
+
+ // [3] Removing the second rule on current upstream does not send the quota.
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_UDP));
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+ inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyUdp));
+ inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyUdp));
+ inOrder.verifyNoMoreInteractions();
+
+ // [4] Removing the last rule on current upstream immediately sends the cleanup stuff.
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(UPSTREAM_IFINDEX, 0, 0, 0, 0));
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_TCP));
+ inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyTcp));
+ inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyTcp));
+ verifyTetherOffloadGetAndClearStats(inOrder, UPSTREAM_IFINDEX);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testAddDevMapRule6() throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+ final Ipv6ForwardingRule ruleA = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+ final Ipv6ForwardingRule ruleB = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
+
+ coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
+ verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
+ eq(new TetherDevValue(UPSTREAM_IFINDEX)));
+ verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
+ eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
+ clearInvocations(mBpfDevMap);
+
+ coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
+ verify(mBpfDevMap, never()).updateEntry(any(), any());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testAddDevMapRule4() throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
+ verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
+ eq(new TetherDevValue(UPSTREAM_IFINDEX)));
+ verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
+ eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
+ clearInvocations(mBpfDevMap);
+
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
+ verify(mBpfDevMap, never()).updateEntry(any(), any());
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
new file mode 100644
index 0000000..d915354
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.networkstack.tethering
+
+import android.net.LinkAddress
+import android.net.MacAddress
+import android.net.TetheredClient
+import android.net.TetheredClient.AddressInfo
+import android.net.TetheringManager.TETHERING_USB
+import android.net.TetheringManager.TETHERING_WIFI
+import android.net.ip.IpServer
+import android.net.wifi.WifiClient
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ConnectedClientsTrackerTest {
+
+ private val server1 = mock(IpServer::class.java)
+ private val server2 = mock(IpServer::class.java)
+ private val servers = listOf(server1, server2)
+
+ private val clock = TestClock(1324L)
+
+ private val client1Addr = MacAddress.fromString("01:23:45:67:89:0A")
+ private val client1 = TetheredClient(client1Addr, listOf(
+ makeAddrInfo("192.168.43.44/32", null /* hostname */, clock.time + 20)),
+ TETHERING_WIFI)
+ private val wifiClient1 = makeWifiClient(client1Addr)
+ private val client2Addr = MacAddress.fromString("02:34:56:78:90:AB")
+ private val client2Exp30AddrInfo = makeAddrInfo(
+ "192.168.43.45/32", "my_hostname", clock.time + 30)
+ private val client2 = TetheredClient(client2Addr, listOf(
+ client2Exp30AddrInfo,
+ makeAddrInfo("2001:db8:12::34/72", "other_hostname", clock.time + 10)),
+ TETHERING_WIFI)
+ private val wifiClient2 = makeWifiClient(client2Addr)
+ private val client3Addr = MacAddress.fromString("03:45:67:89:0A:BC")
+ private val client3 = TetheredClient(client3Addr,
+ listOf(makeAddrInfo("2001:db8:34::34/72", "other_other_hostname", clock.time + 10)),
+ TETHERING_USB)
+
+ private fun makeAddrInfo(addr: String, hostname: String?, expTime: Long) =
+ LinkAddress(addr).let {
+ AddressInfo(LinkAddress(it.address, it.prefixLength, it.flags, it.scope,
+ expTime /* deprecationTime */, expTime /* expirationTime */), hostname)
+ }
+
+ @Test
+ fun testUpdateConnectedClients() {
+ doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+ doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+
+ val tracker = ConnectedClientsTracker(clock)
+ assertFalse(tracker.updateConnectedClients(servers, null))
+
+ // Obtain a lease for client 1
+ doReturn(listOf(client1)).`when`(server1).allLeases
+ assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+ // Client 2 L2-connected, no lease yet
+ val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI)
+ assertSameClients(listOf(client1, client2WithoutAddr),
+ assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2)))
+
+ // Client 2 lease obtained
+ doReturn(listOf(client1, client2)).`when`(server1).allLeases
+ assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null))
+
+ // Client 3 lease obtained
+ doReturn(listOf(client3)).`when`(server2).allLeases
+ assertSameClients(listOf(client1, client2, client3),
+ assertNewClients(tracker, servers, null))
+
+ // Client 2 L2-disconnected
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+ // Client 1 L2-disconnected
+ assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList()))
+
+ // Client 1 comes back
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+ // Leases lost, client 1 still L2-connected
+ doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+ doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+ assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)),
+ assertNewClients(tracker, servers, null))
+ }
+
+ @Test
+ fun testUpdateConnectedClients_LeaseExpiration() {
+ val tracker = ConnectedClientsTracker(clock)
+ doReturn(listOf(client1, client2)).`when`(server1).allLeases
+ doReturn(listOf(client3)).`when`(server2).allLeases
+ assertSameClients(listOf(client1, client2, client3), assertNewClients(
+ tracker, servers, listOf(wifiClient1, wifiClient2)))
+
+ clock.time += 20
+ // Client 3 has no remaining lease: removed
+ val expectedClients = listOf(
+ // Client 1 has no remaining lease but is L2-connected
+ TetheredClient(client1Addr, emptyList(), TETHERING_WIFI),
+ // Client 2 has some expired leases
+ TetheredClient(
+ client2Addr,
+ // Only the "t + 30" address is left, the "t + 10" address expired
+ listOf(client2Exp30AddrInfo),
+ TETHERING_WIFI))
+ assertSameClients(expectedClients, assertNewClients(tracker, servers, null))
+ }
+
+ private fun assertNewClients(
+ tracker: ConnectedClientsTracker,
+ ipServers: Iterable<IpServer>,
+ wifiClients: List<WifiClient>?
+ ): List<TetheredClient> {
+ assertTrue(tracker.updateConnectedClients(ipServers, wifiClients))
+ return tracker.lastTetheredClients
+ }
+
+ private fun assertSameClients(expected: List<TetheredClient>, actual: List<TetheredClient>) {
+ val expectedSet = HashSet(expected)
+ assertEquals(expected.size, expectedSet.size)
+ assertEquals(expectedSet, HashSet(actual))
+ }
+
+ private fun makeWifiClient(macAddr: MacAddress): WifiClient {
+ // Use a mock WifiClient as the constructor is not part of the WiFi module exported API.
+ return mock(WifiClient::class.java).apply { doReturn(macAddr).`when`(this).macAddress }
+ }
+
+ private class TestClock(var time: Long) : ConnectedClientsTracker.Clock() {
+ override fun elapsedRealtime(): Long {
+ return time
+ }
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
new file mode 100644
index 0000000..5ae4b43
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.TetheringConstants.EXTRA_ADD_TETHER_TYPE;
+import static android.net.TetheringConstants.EXTRA_PROVISION_CALLBACK;
+import static android.net.TetheringConstants.EXTRA_RUN_PROVISION;
+import static android.net.TetheringConstants.EXTRA_TETHER_PROVISIONING_RESPONSE;
+import static android.net.TetheringConstants.EXTRA_TETHER_SILENT_PROVISIONING_ACTION;
+import static android.net.TetheringConstants.EXTRA_TETHER_SUBID;
+import static android.net.TetheringConstants.EXTRA_TETHER_UI_PROVISIONING_APP_NAME;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_INVALID;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.util.SharedLog;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.os.SystemProperties;
+import android.os.test.TestLooper;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class EntitlementManagerTest {
+
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+ private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
+ private static final String PROVISIONING_APP_RESPONSE = "app_response";
+ private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
+
+ @Mock private CarrierConfigManager mCarrierConfigManager;
+ @Mock private Context mContext;
+ @Mock private Resources mResources;
+ @Mock private SharedLog mLog;
+ @Mock private PackageManager mPm;
+ @Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
+
+ // Like so many Android system APIs, these cannot be mocked because it is marked final.
+ // We have to use the real versions.
+ private final PersistableBundle mCarrierConfig = new PersistableBundle();
+ private final TestLooper mLooper = new TestLooper();
+ private Context mMockContext;
+ private Runnable mPermissionChangeCallback;
+
+ private WrappedEntitlementManager mEnMgr;
+ private TetheringConfiguration mConfig;
+ private MockitoSession mMockingSession;
+
+ private class MockContext extends BroadcastInterceptingContext {
+ MockContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+ }
+
+ public class WrappedEntitlementManager extends EntitlementManager {
+ public int fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+ public int uiProvisionCount = 0;
+ public int silentProvisionCount = 0;
+
+ public WrappedEntitlementManager(Context ctx, Handler h, SharedLog log,
+ Runnable callback) {
+ super(ctx, h, log, callback);
+ }
+
+ public void reset() {
+ fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+ uiProvisionCount = 0;
+ silentProvisionCount = 0;
+ }
+
+ @Override
+ protected Intent runUiTetherProvisioning(int type,
+ final TetheringConfiguration config, final ResultReceiver receiver) {
+ Intent intent = super.runUiTetherProvisioning(type, config, receiver);
+ assertUiTetherProvisioningIntent(type, config, receiver, intent);
+ uiProvisionCount++;
+ receiver.send(fakeEntitlementResult, null);
+ return intent;
+ }
+
+ private void assertUiTetherProvisioningIntent(int type, final TetheringConfiguration config,
+ final ResultReceiver receiver, final Intent intent) {
+ assertEquals(Settings.ACTION_TETHER_PROVISIONING_UI, intent.getAction());
+ assertEquals(type, intent.getIntExtra(EXTRA_ADD_TETHER_TYPE, TETHERING_INVALID));
+ final String[] appName = intent.getStringArrayExtra(
+ EXTRA_TETHER_UI_PROVISIONING_APP_NAME);
+ assertEquals(PROVISIONING_APP_NAME.length, appName.length);
+ for (int i = 0; i < PROVISIONING_APP_NAME.length; i++) {
+ assertEquals(PROVISIONING_APP_NAME[i], appName[i]);
+ }
+ assertEquals(receiver, intent.getParcelableExtra(EXTRA_PROVISION_CALLBACK));
+ assertEquals(config.activeDataSubId,
+ intent.getIntExtra(EXTRA_TETHER_SUBID, INVALID_SUBSCRIPTION_ID));
+ }
+
+ @Override
+ protected Intent runSilentTetherProvisioning(int type,
+ final TetheringConfiguration config) {
+ Intent intent = super.runSilentTetherProvisioning(type, config);
+ assertSilentTetherProvisioning(type, config, intent);
+ silentProvisionCount++;
+ addDownstreamMapping(type, fakeEntitlementResult);
+ return intent;
+ }
+
+ private void assertSilentTetherProvisioning(int type, final TetheringConfiguration config,
+ final Intent intent) {
+ assertEquals(type, intent.getIntExtra(EXTRA_ADD_TETHER_TYPE, TETHERING_INVALID));
+ assertEquals(true, intent.getBooleanExtra(EXTRA_RUN_PROVISION, false));
+ assertEquals(PROVISIONING_NO_UI_APP_NAME,
+ intent.getStringExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION));
+ assertEquals(PROVISIONING_APP_RESPONSE,
+ intent.getStringExtra(EXTRA_TETHER_PROVISIONING_RESPONSE));
+ assertTrue(intent.hasExtra(EXTRA_PROVISION_CALLBACK));
+ assertEquals(config.activeDataSubId,
+ intent.getIntExtra(EXTRA_TETHER_SUBID, INVALID_SUBSCRIPTION_ID));
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .mockStatic(SystemProperties.class)
+ .mockStatic(DeviceConfig.class)
+ .strictness(Strictness.WARN)
+ .startMocking();
+ // Don't disable tethering provisioning unless requested.
+ doReturn(false).when(
+ () -> SystemProperties.getBoolean(
+ eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean()));
+ doReturn(null).when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), anyString()));
+ doReturn(mPm).when(mContext).getPackageManager();
+ doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
+ doReturn(new PackageInfo()).when(mPm).getPackageInfo(anyString(), anyInt());
+ doReturn(new ModuleInfo()).when(mPm).getModuleInfo(anyString(), anyInt());
+
+ when(mResources.getStringArray(R.array.config_tether_dhcp_range))
+ .thenReturn(new String[0]);
+ when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+ .thenReturn(new String[0]);
+ when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
+ .thenReturn(new String[0]);
+ when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+ .thenReturn(new String[0]);
+ when(mResources.getIntArray(R.array.config_tether_upstream_types))
+ .thenReturn(new int[0]);
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ false);
+ when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
+ when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+
+ mMockContext = new MockContext(mContext);
+ mPermissionChangeCallback = spy(() -> { });
+ mEnMgr = new WrappedEntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
+ mPermissionChangeCallback);
+ mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
+ mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mEnMgr.setTetheringConfigurationFetcher(() -> {
+ return mConfig;
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mMockingSession.finishMocking();
+ }
+
+ private void setupForRequiredProvisioning() {
+ // Produce some acceptable looking provision app setting if requested.
+ when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+ .thenReturn(PROVISIONING_APP_NAME);
+ when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui))
+ .thenReturn(PROVISIONING_NO_UI_APP_NAME);
+ when(mResources.getString(R.string.config_mobile_hotspot_provision_response)).thenReturn(
+ PROVISIONING_APP_RESPONSE);
+ // Act like the CarrierConfigManager is present and ready unless told otherwise.
+ when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
+ .thenReturn(mCarrierConfigManager);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
+ mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+ mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+ mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ }
+
+ @Test
+ public void canRequireProvisioning() {
+ setupForRequiredProvisioning();
+ assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+ }
+
+ @Test
+ public void toleratesCarrierConfigManagerMissing() {
+ setupForRequiredProvisioning();
+ when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
+ .thenReturn(null);
+ mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
+ // Therefore provisioning still be required.
+ assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+ }
+
+ @Test
+ public void toleratesCarrierConfigMissing() {
+ setupForRequiredProvisioning();
+ when(mCarrierConfigManager.getConfig()).thenReturn(null);
+ mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ // We still have a provisioning app configured, so still require provisioning.
+ assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+ }
+
+ @Test
+ public void toleratesCarrierConfigNotLoaded() {
+ setupForRequiredProvisioning();
+ mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
+ // We still have a provisioning app configured, so still require provisioning.
+ assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+ }
+
+ @Test
+ public void provisioningNotRequiredWhenAppNotFound() {
+ setupForRequiredProvisioning();
+ when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+ .thenReturn(null);
+ mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
+ when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+ .thenReturn(new String[] {"malformedApp"});
+ mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
+ }
+
+ @Test
+ public void testRequestLastEntitlementCacheValue() throws Exception {
+ // 1. Entitlement check is not required.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ ResultReceiver receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+
+ setupForRequiredProvisioning();
+ // 2. No cache value and don't need to run entitlement check.
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ // 3. No cache value and ui entitlement check is needed.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mLooper.dispatchAll();
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ // 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement
+ // check.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ // 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mLooper.dispatchAll();
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ // 6. Cache value is TETHER_ERROR_NO_ERROR.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ // 7. Test get value for other downstream type.
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ // 8. Test get value for invalid downstream type.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
+ }
+ };
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ mEnMgr.reset();
+ }
+
+ private void assertPermissionChangeCallback(InOrder inOrder) {
+ inOrder.verify(mPermissionChangeCallback, times(1)).run();
+ }
+
+ private void assertNoPermissionChange(InOrder inOrder) {
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void verifyPermissionResult() {
+ final InOrder inOrder = inOrder(mPermissionChangeCallback);
+ setupForRequiredProvisioning();
+ mEnMgr.notifyUpstream(true);
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+ mLooper.dispatchAll();
+ // Permitted: true -> false
+ assertPermissionChangeCallback(inOrder);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+ mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ // Permitted: false -> false
+ assertNoPermissionChange(inOrder);
+
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+ mLooper.dispatchAll();
+ // Permitted: false -> true
+ assertPermissionChangeCallback(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+ }
+
+ @Test
+ public void verifyPermissionIfAllNotApproved() {
+ final InOrder inOrder = inOrder(mPermissionChangeCallback);
+ setupForRequiredProvisioning();
+ mEnMgr.notifyUpstream(true);
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+ mLooper.dispatchAll();
+ // Permitted: true -> false
+ assertPermissionChangeCallback(inOrder);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+ mLooper.dispatchAll();
+ // Permitted: false -> false
+ assertNoPermissionChange(inOrder);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
+ mLooper.dispatchAll();
+ // Permitted: false -> false
+ assertNoPermissionChange(inOrder);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+ }
+
+ @Test
+ public void verifyPermissionIfAnyApproved() {
+ final InOrder inOrder = inOrder(mPermissionChangeCallback);
+ setupForRequiredProvisioning();
+ mEnMgr.notifyUpstream(true);
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+ mLooper.dispatchAll();
+ // Permitted: true -> true
+ assertNoPermissionChange(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+ mLooper.dispatchAll();
+ // Permitted: true -> true
+ assertNoPermissionChange(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+
+ mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ // Permitted: true -> false
+ assertPermissionChangeCallback(inOrder);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+ }
+
+ @Test
+ public void verifyPermissionWhenProvisioningNotStarted() {
+ final InOrder inOrder = inOrder(mPermissionChangeCallback);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+ assertNoPermissionChange(inOrder);
+ setupForRequiredProvisioning();
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+ assertNoPermissionChange(inOrder);
+ }
+
+ @Test
+ public void testRunTetherProvisioning() {
+ final InOrder inOrder = inOrder(mPermissionChangeCallback);
+ setupForRequiredProvisioning();
+ // 1. start ui provisioning, upstream is mobile
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+ mLooper.dispatchAll();
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ assertEquals(0, mEnMgr.silentProvisionCount);
+ // Permitted: true -> true
+ assertNoPermissionChange(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+ mEnMgr.reset();
+
+ // 2. start no-ui provisioning
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ assertEquals(1, mEnMgr.silentProvisionCount);
+ // Permitted: true -> true
+ assertNoPermissionChange(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+ mEnMgr.reset();
+
+ // 3. tear down mobile, then start ui provisioning
+ mEnMgr.notifyUpstream(false);
+ mLooper.dispatchAll();
+ mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ assertEquals(0, mEnMgr.silentProvisionCount);
+ assertNoPermissionChange(inOrder);
+ mEnMgr.reset();
+
+ // 4. switch upstream back to mobile
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ assertEquals(0, mEnMgr.silentProvisionCount);
+ // Permitted: true -> true
+ assertNoPermissionChange(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+ mEnMgr.reset();
+
+ // 5. tear down mobile, then switch SIM
+ mEnMgr.notifyUpstream(false);
+ mLooper.dispatchAll();
+ mEnMgr.reevaluateSimCardProvisioning(mConfig);
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ assertEquals(0, mEnMgr.silentProvisionCount);
+ assertNoPermissionChange(inOrder);
+ mEnMgr.reset();
+
+ // 6. switch upstream back to mobile again
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ assertEquals(3, mEnMgr.silentProvisionCount);
+ // Permitted: true -> false
+ assertPermissionChangeCallback(inOrder);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+ mEnMgr.reset();
+
+ // 7. start ui provisioning, upstream is mobile, downstream is ethernet
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_ETHERNET, true);
+ mLooper.dispatchAll();
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ assertEquals(0, mEnMgr.silentProvisionCount);
+ // Permitted: false -> true
+ assertPermissionChangeCallback(inOrder);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+ mEnMgr.reset();
+
+ // 8. downstream is invalid
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI_P2P, true);
+ mLooper.dispatchAll();
+ assertEquals(0, mEnMgr.uiProvisionCount);
+ assertEquals(0, mEnMgr.silentProvisionCount);
+ assertNoPermissionChange(inOrder);
+ mEnMgr.reset();
+ }
+
+ @Test
+ public void testCallStopTetheringWhenUiProvisioningFail() {
+ setupForRequiredProvisioning();
+ verify(mEntitlementFailedListener, times(0)).onUiEntitlementFailed(TETHERING_WIFI);
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+ mLooper.dispatchAll();
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ verify(mEntitlementFailedListener, times(1)).onUiEntitlementFailed(TETHERING_WIFI);
+ }
+
+ @Test
+ public void testsetExemptedDownstreamType() throws Exception {
+ setupForRequiredProvisioning();
+ // Cellular upstream is not permitted when no entitlement result.
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+ // If there is exempted downstream and no other non-exempted downstreams, cellular is
+ // permitted.
+ mEnMgr.setExemptedDownstreamType(TETHERING_WIFI);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+
+ // If second downstream run entitlement check fail, cellular upstream is not permitted.
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+ mLooper.dispatchAll();
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+ // When second downstream is down, exempted downstream can use cellular upstream.
+ assertEquals(1, mEnMgr.uiProvisionCount);
+ verify(mEntitlementFailedListener).onUiEntitlementFailed(TETHERING_USB);
+ mEnMgr.stopProvisioningIfNeeded(TETHERING_USB);
+ assertTrue(mEnMgr.isCellularUpstreamPermitted());
+
+ mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+ assertFalse(mEnMgr.isCellularUpstreamPermitted());
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
new file mode 100644
index 0000000..f2b5314
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
+import static android.net.ip.IpServer.STATE_TETHERED;
+
+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.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.RouteInfo;
+import android.net.ip.IpServer;
+import android.net.util.SharedLog;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IPv6TetheringCoordinatorTest {
+ private static final String TEST_DNS_SERVER = "2001:4860:4860::8888";
+ private static final String TEST_INTERFACE = "test_rmnet0";
+ private static final String TEST_IPV6_ADDRESS = "2001:db8::1/64";
+ private static final String TEST_IPV4_ADDRESS = "192.168.100.1/24";
+
+ private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+ private ArrayList<IpServer> mNotifyList;
+
+ @Mock private SharedLog mSharedLog;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
+ mNotifyList = new ArrayList<IpServer>();
+ mIPv6TetheringCoordinator = new IPv6TetheringCoordinator(mNotifyList, mSharedLog);
+ }
+
+ private UpstreamNetworkState createDualStackUpstream(final int transportType) {
+ final Network network = mock(Network.class);
+ final NetworkCapabilities netCap =
+ new NetworkCapabilities.Builder().addTransportType(transportType).build();
+ final InetAddress dns = InetAddresses.parseNumericAddress(TEST_DNS_SERVER);
+ final LinkProperties linkProp = new LinkProperties();
+ linkProp.setInterfaceName(TEST_INTERFACE);
+ linkProp.addLinkAddress(new LinkAddress(TEST_IPV6_ADDRESS));
+ linkProp.addLinkAddress(new LinkAddress(TEST_IPV4_ADDRESS));
+ linkProp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, TEST_INTERFACE, RTN_UNICAST));
+ linkProp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, TEST_INTERFACE,
+ RTN_UNICAST));
+ linkProp.addDnsServer(dns);
+ return new UpstreamNetworkState(linkProp, netCap, network);
+ }
+
+ private void assertOnlyOneV6AddressAndNoV4(LinkProperties lp) {
+ assertEquals(lp.getInterfaceName(), TEST_INTERFACE);
+ assertFalse(lp.hasIpv4Address());
+ final List<LinkAddress> addresses = lp.getLinkAddresses();
+ assertEquals(addresses.size(), 1);
+ final LinkAddress v6Address = addresses.get(0);
+ assertEquals(v6Address, new LinkAddress(TEST_IPV6_ADDRESS));
+ }
+
+ @Test
+ public void testUpdateIpv6Upstream() throws Exception {
+ // 1. Add first IpServer.
+ final IpServer firstServer = mock(IpServer.class);
+ mNotifyList.add(firstServer);
+ mIPv6TetheringCoordinator.addActiveDownstream(firstServer, STATE_TETHERED);
+ verify(firstServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+ verifyNoMoreInteractions(firstServer);
+
+ // 2. Add second IpServer and it would not have ipv6 tethering.
+ final IpServer secondServer = mock(IpServer.class);
+ mNotifyList.add(secondServer);
+ mIPv6TetheringCoordinator.addActiveDownstream(secondServer, STATE_LOCAL_ONLY);
+ verifyNoMoreInteractions(secondServer);
+ reset(firstServer, secondServer);
+
+ // 3. No upstream.
+ mIPv6TetheringCoordinator.updateUpstreamNetworkState(null);
+ verify(secondServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+ reset(firstServer, secondServer);
+
+ // 4. Update ipv6 mobile upstream.
+ final UpstreamNetworkState mobileUpstream = createDualStackUpstream(TRANSPORT_CELLULAR);
+ final ArgumentCaptor<LinkProperties> lp = ArgumentCaptor.forClass(LinkProperties.class);
+ mIPv6TetheringCoordinator.updateUpstreamNetworkState(mobileUpstream);
+ verify(firstServer).sendMessage(eq(IpServer.CMD_IPV6_TETHER_UPDATE), eq(-1), eq(0),
+ lp.capture());
+ final LinkProperties v6OnlyLink = lp.getValue();
+ assertOnlyOneV6AddressAndNoV4(v6OnlyLink);
+ verifyNoMoreInteractions(firstServer);
+ verifyNoMoreInteractions(secondServer);
+ reset(firstServer, secondServer);
+
+ // 5. Remove first IpServer.
+ mNotifyList.remove(firstServer);
+ mIPv6TetheringCoordinator.removeActiveDownstream(firstServer);
+ verify(firstServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+ verify(secondServer).sendMessage(eq(IpServer.CMD_IPV6_TETHER_UPDATE), eq(-1), eq(0),
+ lp.capture());
+ final LinkProperties localOnlyLink = lp.getValue();
+ assertNotNull(localOnlyLink);
+ assertNotEquals(localOnlyLink, v6OnlyLink);
+ reset(firstServer, secondServer);
+
+ // 6. Remove second IpServer.
+ mNotifyList.remove(secondServer);
+ mIPv6TetheringCoordinator.removeActiveDownstream(secondServer);
+ verifyNoMoreInteractions(firstServer);
+ verify(secondServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
new file mode 100644
index 0000000..071a290
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
@@ -0,0 +1,72 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.Manifest.permission.WRITE_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.ITetheringConnector;
+import android.os.Binder;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class MockTetheringService extends TetheringService {
+ private final Tethering mTethering = mock(Tethering.class);
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new MockTetheringConnector(super.onBind(intent));
+ }
+
+ @Override
+ public Tethering makeTethering(TetheringDependencies deps) {
+ return mTethering;
+ }
+
+ @Override
+ boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
+ @NonNull String callingPackage, @Nullable String callingAttributionTag,
+ boolean throwException) {
+ // Test this does not verify the calling package / UID, as calling package could be shell
+ // and not match the UID.
+ return context.checkCallingOrSelfPermission(WRITE_SETTINGS) == PERMISSION_GRANTED;
+ }
+
+ public Tethering getTethering() {
+ return mTethering;
+ }
+
+ public class MockTetheringConnector extends Binder {
+ final IBinder mBase;
+ MockTetheringConnector(IBinder base) {
+ mBase = base;
+ }
+
+ public ITetheringConnector getTetheringConnector() {
+ return ITetheringConnector.Stub.asInterface(mBase);
+ }
+
+ public MockTetheringService getService() {
+ return MockTetheringService.this;
+ }
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
new file mode 100644
index 0000000..d800816
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -0,0 +1,916 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+
+import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
+import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
+import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.testutils.MiscAsserts.assertContainsAll;
+import static com.android.testutils.MiscAsserts.assertThrows;
+import static com.android.testutils.NetworkStatsUtilsKt.assertNetworkStatsEquals;
+
+import static junit.framework.Assert.assertNotNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkStats;
+import android.net.NetworkStats.Entry;
+import android.net.RouteInfo;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.testutils.TestableNetworkStatsProviderCbBinder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OffloadControllerTest {
+ private static final String RNDIS0 = "test_rndis0";
+ private static final String RMNET0 = "test_rmnet_data0";
+ private static final String WLAN0 = "test_wlan0";
+
+ private static final String IPV6_LINKLOCAL = "fe80::/64";
+ private static final String IPV6_DOC_PREFIX = "2001:db8::/64";
+ private static final String IPV6_DISCARD_PREFIX = "100::/64";
+ private static final String USB_PREFIX = "192.168.42.0/24";
+ private static final String WIFI_PREFIX = "192.168.43.0/24";
+ private static final long WAIT_FOR_IDLE_TIMEOUT = 2 * 1000;
+
+ @Mock private OffloadHardwareInterface mHardware;
+ @Mock private ApplicationInfo mApplicationInfo;
+ @Mock private Context mContext;
+ @Mock private NetworkStatsManager mStatsManager;
+ @Mock private TetheringConfiguration mTetherConfig;
+ // Late init since methods must be called by the thread that created this object.
+ private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
+ private OffloadController.OffloadTetheringStatsProvider mTetherStatsProvider;
+ private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
+ ArgumentCaptor.forClass(ArrayList.class);
+ private final ArgumentCaptor<OffloadHardwareInterface.ControlCallback> mControlCallbackCaptor =
+ ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class);
+ private MockContentResolver mContentResolver;
+ private final TestLooper mTestLooper = new TestLooper();
+ private OffloadController.Dependencies mDeps = new OffloadController.Dependencies() {
+ @Override
+ public TetheringConfiguration getTetherConfig() {
+ return mTetherConfig;
+ }
+ };
+
+ @Before public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo);
+ when(mContext.getPackageName()).thenReturn("OffloadControllerTest");
+ mContentResolver = new MockContentResolver(mContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ when(mContext.getContentResolver()).thenReturn(mContentResolver);
+ FakeSettingsProvider.clearSettingsProvider();
+ when(mTetherConfig.getOffloadPollInterval()).thenReturn(-1); // Disabled.
+ }
+
+ @After public void tearDown() throws Exception {
+ FakeSettingsProvider.clearSettingsProvider();
+ }
+
+ private void setupFunctioningHardwareInterface(int controlVersion) {
+ when(mHardware.initOffloadConfig()).thenReturn(true);
+ when(mHardware.initOffloadControl(mControlCallbackCaptor.capture()))
+ .thenReturn(controlVersion);
+ when(mHardware.setUpstreamParameters(anyString(), any(), any(), any())).thenReturn(true);
+ when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats());
+ when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
+ when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
+ }
+
+ private void enableOffload() {
+ Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
+ }
+
+ private void setOffloadPollInterval(int interval) {
+ when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
+ }
+
+ private void waitForIdle() {
+ mTestLooper.dispatchAll();
+ }
+
+ private OffloadController makeOffloadController() throws Exception {
+ OffloadController offload = new OffloadController(new Handler(mTestLooper.getLooper()),
+ mHardware, mContentResolver, mStatsManager, new SharedLog("test"), mDeps);
+ final ArgumentCaptor<OffloadController.OffloadTetheringStatsProvider>
+ tetherStatsProviderCaptor =
+ ArgumentCaptor.forClass(OffloadController.OffloadTetheringStatsProvider.class);
+ verify(mStatsManager).registerNetworkStatsProvider(anyString(),
+ tetherStatsProviderCaptor.capture());
+ reset(mStatsManager);
+ mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
+ assertNotNull(mTetherStatsProvider);
+ mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
+ mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
+ return offload;
+ }
+
+ @Test
+ public void testStartStop() throws Exception {
+ stopOffloadController(
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/));
+ stopOffloadController(
+ startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/));
+ }
+
+ @NonNull
+ private OffloadController startOffloadController(int controlVersion, boolean expectStart)
+ throws Exception {
+ setupFunctioningHardwareInterface(controlVersion);
+ final OffloadController offload = makeOffloadController();
+ offload.start();
+
+ final InOrder inOrder = inOrder(mHardware);
+ inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
+ inOrder.verify(mHardware, times(expectStart ? 1 : 0)).initOffloadConfig();
+ inOrder.verify(mHardware, times(expectStart ? 1 : 0)).initOffloadControl(
+ any(OffloadHardwareInterface.ControlCallback.class));
+ inOrder.verifyNoMoreInteractions();
+ // Clear counters only instead of whole mock to preserve the mocking setup.
+ clearInvocations(mHardware);
+ return offload;
+ }
+
+ private void stopOffloadController(final OffloadController offload) throws Exception {
+ final InOrder inOrder = inOrder(mHardware);
+ offload.stop();
+ inOrder.verify(mHardware, times(1)).stopOffloadControl();
+ inOrder.verifyNoMoreInteractions();
+ reset(mHardware);
+ }
+
+ @Test
+ public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception {
+ when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1);
+ assertThrows(SettingNotFoundException.class, () ->
+ Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/);
+ }
+
+ @Test
+ public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception {
+ when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0);
+ assertThrows(SettingNotFoundException.class, () ->
+ Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+ }
+
+ @Test
+ public void testSettingsAllowsStart() throws Exception {
+ Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+ }
+
+ @Test
+ public void testSettingsDisablesStart() throws Exception {
+ Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1);
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/);
+ }
+
+ @Test
+ public void testSetUpstreamLinkPropertiesWorking() throws Exception {
+ enableOffload();
+ final OffloadController offload =
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+
+ // In reality, the UpstreamNetworkMonitor would have passed down to us
+ // a covering set of local prefixes representing a minimum essential
+ // set plus all the prefixes on networks with network agents.
+ //
+ // We simulate that there, and then add upstream elements one by one
+ // and watch what happens.
+ final Set<IpPrefix> minimumLocalPrefixes = new HashSet<>();
+ for (String s : new String[]{
+ "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) {
+ minimumLocalPrefixes.add(new IpPrefix(s));
+ }
+ offload.setLocalPrefixes(minimumLocalPrefixes);
+ final InOrder inOrder = inOrder(mHardware);
+ inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
+ ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
+ assertEquals(4, localPrefixes.size());
+ assertContainsAll(localPrefixes,
+ "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
+ inOrder.verifyNoMoreInteractions();
+
+ offload.setUpstreamLinkProperties(null);
+ // No change in local addresses means no call to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ // This LinkProperties value does not differ from the default upstream.
+ // There should be no extraneous call to setUpstreamParameters().
+ inOrder.verify(mHardware, never()).setUpstreamParameters(
+ anyObject(), anyObject(), anyObject(), anyObject());
+ inOrder.verifyNoMoreInteractions();
+
+ final LinkProperties lp = new LinkProperties();
+
+ final String testIfName = "rmnet_data17";
+ lp.setInterfaceName(testIfName);
+ offload.setUpstreamLinkProperties(lp);
+ // No change in local addresses means no call to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(null), eq(null), eq(null));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ final String ipv4Addr = "192.0.2.5";
+ final String linkAddr = ipv4Addr + "/24";
+ lp.addLinkAddress(new LinkAddress(linkAddr));
+ lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, null, RTN_UNICAST));
+ offload.setUpstreamLinkProperties(lp);
+ // IPv4 prefixes and addresses on the upstream are simply left as whole
+ // prefixes (already passed in from UpstreamNetworkMonitor code). If a
+ // tethering client sends traffic to the IPv4 default router or other
+ // clients on the upstream this will not be hardware-forwarded, and that
+ // should be fine for now. Ergo: no change in local addresses, no call
+ // to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(ipv4Addr), eq(null), eq(null));
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ final String ipv4Gateway = "192.0.2.1";
+ lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv4Gateway), null, RTN_UNICAST));
+ offload.setUpstreamLinkProperties(lp);
+ // No change in local addresses means no call to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), eq(null));
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ final String ipv6Gw1 = "fe80::cafe";
+ lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv6Gw1), null, RTN_UNICAST));
+ offload.setUpstreamLinkProperties(lp);
+ // No change in local addresses means no call to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+ ArrayList<String> v6gws = mStringArrayCaptor.getValue();
+ assertEquals(1, v6gws.size());
+ assertTrue(v6gws.contains(ipv6Gw1));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ final String ipv6Gw2 = "fe80::d00d";
+ lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv6Gw2), null, RTN_UNICAST));
+ offload.setUpstreamLinkProperties(lp);
+ // No change in local addresses means no call to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+ v6gws = mStringArrayCaptor.getValue();
+ assertEquals(2, v6gws.size());
+ assertTrue(v6gws.contains(ipv6Gw1));
+ assertTrue(v6gws.contains(ipv6Gw2));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ final LinkProperties stacked = new LinkProperties();
+ stacked.setInterfaceName("stacked");
+ stacked.addLinkAddress(new LinkAddress("192.0.2.129/25"));
+ stacked.addRoute(new RouteInfo(null, InetAddress.getByName("192.0.2.254"), null,
+ RTN_UNICAST));
+ stacked.addRoute(new RouteInfo(null, InetAddress.getByName("fe80::bad:f00"), null,
+ RTN_UNICAST));
+ assertTrue(lp.addStackedLink(stacked));
+ offload.setUpstreamLinkProperties(lp);
+ // No change in local addresses means no call to setLocalPrefixes().
+ inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+ v6gws = mStringArrayCaptor.getValue();
+ assertEquals(2, v6gws.size());
+ assertTrue(v6gws.contains(ipv6Gw1));
+ assertTrue(v6gws.contains(ipv6Gw2));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ // Add in some IPv6 upstream info. When there is a tethered downstream
+ // making use of the IPv6 prefix we would expect to see the /64 route
+ // removed from "local prefixes" and /128s added for the upstream IPv6
+ // addresses. This is not yet implemented, and for now we simply
+ // expect to see these /128s.
+ lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null, RTN_UNICAST));
+ // "2001:db8::/64" plus "assigned" ASCII in hex
+ lp.addLinkAddress(new LinkAddress("2001:db8::6173:7369:676e:6564/64"));
+ // "2001:db8::/64" plus "random" ASCII in hex
+ lp.addLinkAddress(new LinkAddress("2001:db8::7261:6e64:6f6d/64"));
+ offload.setUpstreamLinkProperties(lp);
+ inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
+ localPrefixes = mStringArrayCaptor.getValue();
+ assertEquals(6, localPrefixes.size());
+ assertContainsAll(localPrefixes,
+ "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64",
+ "2001:db8::6173:7369:676e:6564/128", "2001:db8::7261:6e64:6f6d/128");
+ // The relevant parts of the LinkProperties have not changed, but at the
+ // moment we do not de-dup upstream LinkProperties this carefully.
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ v6gws = mStringArrayCaptor.getValue();
+ assertEquals(2, v6gws.size());
+ assertTrue(v6gws.contains(ipv6Gw1));
+ assertTrue(v6gws.contains(ipv6Gw2));
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+ inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+ inOrder.verifyNoMoreInteractions();
+
+ // Completely identical LinkProperties updates are de-duped.
+ offload.setUpstreamLinkProperties(lp);
+ // This LinkProperties value does not differ from the default upstream.
+ // There should be no extraneous call to setUpstreamParameters().
+ inOrder.verify(mHardware, never()).setUpstreamParameters(
+ anyObject(), anyObject(), anyObject(), anyObject());
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ private static @NonNull Entry buildTestEntry(@NonNull OffloadController.StatsType how,
+ @NonNull String iface, long rxBytes, long txBytes) {
+ return new Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING, SET_DEFAULT,
+ TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, 0L,
+ txBytes, 0L, 0L);
+ }
+
+ @Test
+ public void testGetForwardedStats() throws Exception {
+ enableOffload();
+ final OffloadController offload =
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+
+ final String ethernetIface = "eth1";
+ final String mobileIface = "rmnet_data0";
+
+ when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
+ new ForwardedStats(12345, 54321));
+ when(mHardware.getForwardedStats(eq(mobileIface))).thenReturn(
+ new ForwardedStats(999, 99999));
+
+ final InOrder inOrder = inOrder(mHardware);
+
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(ethernetIface);
+ offload.setUpstreamLinkProperties(lp);
+ // Previous upstream was null, so no stats are fetched.
+ inOrder.verify(mHardware, never()).getForwardedStats(any());
+
+ lp.setInterfaceName(mobileIface);
+ offload.setUpstreamLinkProperties(lp);
+ // Expect that we fetch stats from the previous upstream.
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface));
+
+ lp.setInterfaceName(ethernetIface);
+ offload.setUpstreamLinkProperties(lp);
+ // Expect that we fetch stats from the previous upstream.
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(mobileIface));
+
+ // Verify that the fetched stats are stored.
+ final NetworkStats ifaceStats = mTetherStatsProvider.getTetherStats(STATS_PER_IFACE);
+ final NetworkStats uidStats = mTetherStatsProvider.getTetherStats(STATS_PER_UID);
+ final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 999, 99999))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 12345, 54321));
+
+ final NetworkStats expectedUidStats = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999))
+ .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 12345, 54321));
+
+ assertNetworkStatsEquals(expectedIfaceStats, ifaceStats);
+ assertNetworkStatsEquals(expectedUidStats, uidStats);
+
+ // Force pushing stats update to verify the stats reported.
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
+
+ when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
+ new ForwardedStats(100000, 100000));
+ offload.setUpstreamLinkProperties(null);
+ // Expect that we first clear the HAL's upstream parameters.
+ inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+ eq(""), eq("0.0.0.0"), eq("0.0.0.0"), eq(null));
+ // Expect that we fetch stats from the previous upstream.
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface));
+
+ // There is no current upstream, so no stats are fetched.
+ inOrder.verify(mHardware, never()).getForwardedStats(any());
+ inOrder.verifyNoMoreInteractions();
+
+ // Verify that the stored stats is accumulated.
+ final NetworkStats ifaceStatsAccu = mTetherStatsProvider.getTetherStats(STATS_PER_IFACE);
+ final NetworkStats uidStatsAccu = mTetherStatsProvider.getTetherStats(STATS_PER_UID);
+ final NetworkStats expectedIfaceStatsAccu = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 999, 99999))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 112345, 154321));
+
+ final NetworkStats expectedUidStatsAccu = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999))
+ .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 112345, 154321));
+
+ assertNetworkStatsEquals(expectedIfaceStatsAccu, ifaceStatsAccu);
+ assertNetworkStatsEquals(expectedUidStatsAccu, uidStatsAccu);
+
+ // Verify that only diff of stats is reported.
+ mTetherStatsProvider.pushTetherStats();
+ final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 0, 0))
+ .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 100000, 100000));
+
+ final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 0, 0))
+ .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 100000, 100000));
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
+ expectedUidStatsDiff);
+ }
+
+ /**
+ * Test OffloadController with different combinations of HAL and framework versions can set
+ * data warning and/or limit correctly.
+ */
+ @Test
+ public void testSetDataWarningAndLimit() throws Exception {
+ // Verify the OffloadController is called by R framework, where the framework doesn't send
+ // warning.
+ checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_0);
+ checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_1);
+ // Verify the OffloadController is called by S+ framework, where the framework sends
+ // warning along with limit.
+ checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_0);
+ checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_1);
+ }
+
+ private void checkSetDataWarningAndLimit(boolean isProviderSetWarning, int controlVersion)
+ throws Exception {
+ enableOffload();
+ final OffloadController offload =
+ startOffloadController(controlVersion, true /*expectStart*/);
+
+ final String ethernetIface = "eth1";
+ final String mobileIface = "rmnet_data0";
+ final long ethernetLimit = 12345;
+ final long mobileWarning = 123456;
+ final long mobileLimit = 12345678;
+
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(ethernetIface);
+
+ final InOrder inOrder = inOrder(mHardware);
+ when(mHardware.setUpstreamParameters(
+ any(), any(), any(), any())).thenReturn(true);
+ when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
+ when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
+ offload.setUpstreamLinkProperties(lp);
+ // Applying an interface sends the initial quota to the hardware.
+ if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE,
+ Long.MAX_VALUE);
+ } else {
+ inOrder.verify(mHardware).setDataLimit(ethernetIface, Long.MAX_VALUE);
+ }
+ inOrder.verifyNoMoreInteractions();
+
+ // Verify that set to unlimited again won't cause duplicated calls to the hardware.
+ if (isProviderSetWarning) {
+ mTetherStatsProvider.onSetWarningAndLimit(ethernetIface,
+ NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED);
+ } else {
+ mTetherStatsProvider.onSetLimit(ethernetIface, NetworkStatsProvider.QUOTA_UNLIMITED);
+ }
+ waitForIdle();
+ inOrder.verifyNoMoreInteractions();
+
+ // Applying an interface quota to the current upstream immediately sends it to the hardware.
+ if (isProviderSetWarning) {
+ mTetherStatsProvider.onSetWarningAndLimit(ethernetIface,
+ NetworkStatsProvider.QUOTA_UNLIMITED, ethernetLimit);
+ } else {
+ mTetherStatsProvider.onSetLimit(ethernetIface, ethernetLimit);
+ }
+ waitForIdle();
+ if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE,
+ ethernetLimit);
+ } else {
+ inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit);
+ }
+ inOrder.verifyNoMoreInteractions();
+
+ // Applying an interface quota to another upstream does not take any immediate action.
+ if (isProviderSetWarning) {
+ mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
+ } else {
+ mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+ }
+ waitForIdle();
+ if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(),
+ anyLong());
+ } else {
+ inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
+ }
+
+ // Switching to that upstream causes the quota to be applied if the parameters were applied
+ // correctly.
+ lp.setInterfaceName(mobileIface);
+ offload.setUpstreamLinkProperties(lp);
+ waitForIdle();
+ if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface,
+ isProviderSetWarning ? mobileWarning : Long.MAX_VALUE,
+ mobileLimit);
+ } else {
+ inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit);
+ }
+
+ // Setting a limit of NetworkStatsProvider.QUOTA_UNLIMITED causes the limit to be set
+ // to Long.MAX_VALUE.
+ if (isProviderSetWarning) {
+ mTetherStatsProvider.onSetWarningAndLimit(mobileIface,
+ NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED);
+ } else {
+ mTetherStatsProvider.onSetLimit(mobileIface, NetworkStatsProvider.QUOTA_UNLIMITED);
+ }
+ waitForIdle();
+ if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface, Long.MAX_VALUE,
+ Long.MAX_VALUE);
+ } else {
+ inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE);
+ }
+
+ // If setting upstream parameters fails, then the data warning and limit is not set.
+ when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(false);
+ lp.setInterfaceName(ethernetIface);
+ offload.setUpstreamLinkProperties(lp);
+ if (isProviderSetWarning) {
+ mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
+ } else {
+ mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+ }
+ waitForIdle();
+ inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
+ inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(),
+ anyLong());
+
+ // If setting the data warning and/or limit fails while changing upstreams, offload is
+ // stopped.
+ when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true);
+ when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(false);
+ when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(false);
+ lp.setInterfaceName(mobileIface);
+ offload.setUpstreamLinkProperties(lp);
+ if (isProviderSetWarning) {
+ mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit);
+ } else {
+ mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit);
+ }
+ waitForIdle();
+ inOrder.verify(mHardware).getForwardedStats(ethernetIface);
+ inOrder.verify(mHardware).stopOffloadControl();
+ }
+
+ @Test
+ public void testDataWarningAndLimitCallback() throws Exception {
+ enableOffload();
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+
+ OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+ callback.onStoppedLimitReached();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated();
+ mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
+
+ startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/);
+ callback = mControlCallbackCaptor.getValue();
+ callback.onWarningReached();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated();
+ mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
+ }
+
+ @Test
+ public void testAddRemoveDownstreams() throws Exception {
+ enableOffload();
+ final OffloadController offload =
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+ final InOrder inOrder = inOrder(mHardware);
+
+ // Tethering makes several calls to setLocalPrefixes() before add/remove
+ // downstream calls are made. This is not tested here; only the behavior
+ // of notifyDownstreamLinkProperties() and removeDownstreamInterface()
+ // are tested.
+
+ // [1] USB tethering is started.
+ final LinkProperties usbLinkProperties = new LinkProperties();
+ usbLinkProperties.setInterfaceName(RNDIS0);
+ usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
+ usbLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(USB_PREFIX), null, null, RTN_UNICAST));
+ offload.notifyDownstreamLinkProperties(usbLinkProperties);
+ inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, USB_PREFIX);
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Routes for IPv6 link-local prefixes should never be added.
+ usbLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(IPV6_LINKLOCAL), null, null, RTN_UNICAST));
+ offload.notifyDownstreamLinkProperties(usbLinkProperties);
+ inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
+ inOrder.verifyNoMoreInteractions();
+
+ // [3] Add an IPv6 prefix for good measure. Only new offload-able
+ // prefixes should be passed to the HAL.
+ usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+ usbLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, null, RTN_UNICAST));
+ offload.notifyDownstreamLinkProperties(usbLinkProperties);
+ inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
+ inOrder.verifyNoMoreInteractions();
+
+ // [4] Adding addresses doesn't affect notifyDownstreamLinkProperties().
+ // The address is passed in by a separate setLocalPrefixes() invocation.
+ usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::2/64"));
+ offload.notifyDownstreamLinkProperties(usbLinkProperties);
+ inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
+
+ // [5] Differences in local routes are converted into addDownstream()
+ // and removeDownstream() invocations accordingly.
+ usbLinkProperties.removeRoute(
+ new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, RNDIS0, RTN_UNICAST));
+ usbLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(IPV6_DISCARD_PREFIX), null, null, RTN_UNICAST));
+ offload.notifyDownstreamLinkProperties(usbLinkProperties);
+ inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
+ inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
+ inOrder.verifyNoMoreInteractions();
+
+ // [6] Removing a downstream interface which was never added causes no
+ // interactions with the HAL.
+ offload.removeDownstreamInterface(WLAN0);
+ inOrder.verifyNoMoreInteractions();
+
+ // [7] Removing an active downstream removes all remaining prefixes.
+ offload.removeDownstreamInterface(RNDIS0);
+ inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, USB_PREFIX);
+ inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testControlCallbackOnStoppedUnsupportedFetchesAllStats() throws Exception {
+ enableOffload();
+ final OffloadController offload =
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+
+ // Pretend to set a few different upstreams (only the interface name
+ // matters for this test; we're ignoring IP and route information).
+ final LinkProperties upstreamLp = new LinkProperties();
+ for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) {
+ upstreamLp.setInterfaceName(ifname);
+ offload.setUpstreamLinkProperties(upstreamLp);
+ }
+
+ // Clear invocation history, especially the getForwardedStats() calls
+ // that happen with setUpstreamParameters().
+ clearInvocations(mHardware);
+
+ OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+ callback.onStoppedUnsupported();
+
+ // Verify forwarded stats behaviour.
+ verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
+ verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
+ // TODO: verify the exact stats reported.
+ mTetherStatsProviderCb.expectNotifyStatsUpdated();
+ mTetherStatsProviderCb.assertNoCallback();
+ verifyNoMoreInteractions(mHardware);
+ }
+
+ @Test
+ public void testControlCallbackOnSupportAvailableFetchesAllStatsAndPushesAllParameters()
+ throws Exception {
+ enableOffload();
+ final OffloadController offload =
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+
+ // Pretend to set a few different upstreams (only the interface name
+ // matters for this test; we're ignoring IP and route information).
+ final LinkProperties upstreamLp = new LinkProperties();
+ for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) {
+ upstreamLp.setInterfaceName(ifname);
+ offload.setUpstreamLinkProperties(upstreamLp);
+ }
+
+ // Pretend that some local prefixes and downstreams have been added
+ // (and removed, for good measure).
+ final Set<IpPrefix> minimumLocalPrefixes = new HashSet<>();
+ for (String s : new String[]{
+ "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) {
+ minimumLocalPrefixes.add(new IpPrefix(s));
+ }
+ offload.setLocalPrefixes(minimumLocalPrefixes);
+
+ final LinkProperties usbLinkProperties = new LinkProperties();
+ usbLinkProperties.setInterfaceName(RNDIS0);
+ usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
+ usbLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(USB_PREFIX), null, null, RTN_UNICAST));
+ offload.notifyDownstreamLinkProperties(usbLinkProperties);
+
+ final LinkProperties wifiLinkProperties = new LinkProperties();
+ wifiLinkProperties.setInterfaceName(WLAN0);
+ wifiLinkProperties.addLinkAddress(new LinkAddress("192.168.43.1/24"));
+ wifiLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(WIFI_PREFIX), null, null, RTN_UNICAST));
+ wifiLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix(IPV6_LINKLOCAL), null, null, RTN_UNICAST));
+ // Use a benchmark prefix (RFC 5180 + erratum), since the documentation
+ // prefix is included in the excluded prefix list.
+ wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::1/64"));
+ wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::2/64"));
+ wifiLinkProperties.addRoute(
+ new RouteInfo(new IpPrefix("2001:2::/64"), null, null, RTN_UNICAST));
+ offload.notifyDownstreamLinkProperties(wifiLinkProperties);
+
+ offload.removeDownstreamInterface(RNDIS0);
+
+ // Clear invocation history, especially the getForwardedStats() calls
+ // that happen with setUpstreamParameters().
+ clearInvocations(mHardware);
+
+ OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+ callback.onSupportAvailable();
+
+ // Verify forwarded stats behaviour.
+ verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
+ verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
+ mTetherStatsProviderCb.expectNotifyStatsUpdated();
+ mTetherStatsProviderCb.assertNoCallback();
+
+ // TODO: verify local prefixes and downstreams are also pushed to the HAL.
+ verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
+ ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
+ assertEquals(4, localPrefixes.size());
+ assertContainsAll(localPrefixes,
+ // TODO: The logic to find and exclude downstream IP prefixes
+ // is currently in Tethering's OffloadWrapper but must be moved
+ // into OffloadController proper. After this, also check for:
+ // "192.168.43.1/32", "2001:2::1/128", "2001:2::2/128"
+ "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
+ verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "192.168.43.0/24");
+ verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "2001:2::/64");
+ verify(mHardware, times(1)).setUpstreamParameters(eq(RMNET0), any(), any(), any());
+ verify(mHardware, times(1)).setDataLimit(eq(RMNET0), anyLong());
+ verifyNoMoreInteractions(mHardware);
+ }
+
+ @Test
+ public void testOnSetAlert() throws Exception {
+ enableOffload();
+ setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ final OffloadController offload =
+ startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
+
+ // Initialize with fake eth upstream.
+ final String ethernetIface = "eth1";
+ InOrder inOrder = inOrder(mHardware);
+ offload.setUpstreamLinkProperties(makeEthernetLinkProperties());
+ // Previous upstream was null, so no stats are fetched.
+ inOrder.verify(mHardware, never()).getForwardedStats(any());
+
+ // Verify that set quota to 0 will immediately triggers an callback.
+ mTetherStatsProvider.onSetAlert(0);
+ waitForIdle();
+ mTetherStatsProviderCb.expectNotifyAlertReached();
+
+ // Verify that notifyAlertReached never fired if quota is not yet reached.
+ when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
+ new ForwardedStats(0, 0));
+ mTetherStatsProvider.onSetAlert(100);
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ mTetherStatsProviderCb.assertNoCallback();
+
+ // Verify that notifyAlertReached fired when quota is reached.
+ when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
+ new ForwardedStats(50, 50));
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ mTetherStatsProviderCb.expectNotifyAlertReached();
+
+ // Verify that set quota with UNLIMITED won't trigger any callback, and won't fetch
+ // any stats since the polling is stopped.
+ reset(mHardware);
+ mTetherStatsProvider.onSetAlert(NetworkStatsProvider.QUOTA_UNLIMITED);
+ mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ waitForIdle();
+ mTetherStatsProviderCb.assertNoCallback();
+ verify(mHardware, never()).getForwardedStats(any());
+ }
+
+ private static LinkProperties makeEthernetLinkProperties() {
+ final String ethernetIface = "eth1";
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(ethernetIface);
+ return lp;
+ }
+
+ private void checkSoftwarePollingUsed(int controlVersion) throws Exception {
+ enableOffload();
+ setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ OffloadController offload =
+ startOffloadController(controlVersion, true /*expectStart*/);
+ offload.setUpstreamLinkProperties(makeEthernetLinkProperties());
+ mTetherStatsProvider.onSetAlert(0);
+ waitForIdle();
+ if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) {
+ mTetherStatsProviderCb.assertNoCallback();
+ } else {
+ mTetherStatsProviderCb.expectNotifyAlertReached();
+ }
+ verify(mHardware, never()).getForwardedStats(any());
+ }
+
+ @Test
+ public void testSoftwarePollingUsed() throws Exception {
+ checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_0);
+ checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_1);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
new file mode 100644
index 0000000..a8b3b92
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.util.TetheringUtils.uint16;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.SOCK_STREAM;
+
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.tetheroffload.config.V1_0.IOffloadConfig;
+import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
+import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
+import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
+import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
+import android.hardware.tetheroffload.control.V1_1.OffloadCallbackEvent;
+import android.net.netlink.StructNfGenMsg;
+import android.net.netlink.StructNlMsgHdr;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.NativeHandle;
+import android.os.test.TestLooper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class OffloadHardwareInterfaceTest {
+ private static final String RMNET0 = "test_rmnet_data0";
+
+ private final TestLooper mTestLooper = new TestLooper();
+
+ private OffloadHardwareInterface mOffloadHw;
+ private ITetheringOffloadCallback mTetheringOffloadCallback;
+ private OffloadHardwareInterface.ControlCallback mControlCallback;
+
+ @Mock private IOffloadConfig mIOffloadConfig;
+ private IOffloadControl mIOffloadControl;
+ @Mock private NativeHandle mNativeHandle;
+
+ // Random values to test Netlink message.
+ private static final short TEST_TYPE = 184;
+ private static final short TEST_FLAGS = 263;
+
+ class MyDependencies extends OffloadHardwareInterface.Dependencies {
+ private final int mMockControlVersion;
+ MyDependencies(SharedLog log, final int mockControlVersion) {
+ super(log);
+ mMockControlVersion = mockControlVersion;
+ }
+
+ @Override
+ public IOffloadConfig getOffloadConfig() {
+ return mIOffloadConfig;
+ }
+
+ @Override
+ public Pair<IOffloadControl, Integer> getOffloadControl() {
+ switch (mMockControlVersion) {
+ case OFFLOAD_HAL_VERSION_1_0:
+ mIOffloadControl = mock(IOffloadControl.class);
+ break;
+ case OFFLOAD_HAL_VERSION_1_1:
+ mIOffloadControl =
+ mock(android.hardware.tetheroffload.control.V1_1.IOffloadControl.class);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid offload control version "
+ + mMockControlVersion);
+ }
+ return new Pair<IOffloadControl, Integer>(mIOffloadControl, mMockControlVersion);
+ }
+
+ @Override
+ public NativeHandle createConntrackSocket(final int groups) {
+ return mNativeHandle;
+ }
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mControlCallback = spy(new OffloadHardwareInterface.ControlCallback());
+ }
+
+ private void startOffloadHardwareInterface(int controlVersion) throws Exception {
+ final SharedLog log = new SharedLog("test");
+ mOffloadHw = new OffloadHardwareInterface(new Handler(mTestLooper.getLooper()), log,
+ new MyDependencies(log, controlVersion));
+ mOffloadHw.initOffloadConfig();
+ mOffloadHw.initOffloadControl(mControlCallback);
+ final ArgumentCaptor<ITetheringOffloadCallback> mOffloadCallbackCaptor =
+ ArgumentCaptor.forClass(ITetheringOffloadCallback.class);
+ verify(mIOffloadControl).initOffload(mOffloadCallbackCaptor.capture(), any());
+ mTetheringOffloadCallback = mOffloadCallbackCaptor.getValue();
+ }
+
+ @Test
+ public void testGetForwardedStats() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ final OffloadHardwareInterface.ForwardedStats stats = mOffloadHw.getForwardedStats(RMNET0);
+ verify(mIOffloadControl).getForwardedStats(eq(RMNET0), any());
+ assertNotNull(stats);
+ }
+
+ @Test
+ public void testSetLocalPrefixes() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ final ArrayList<String> localPrefixes = new ArrayList<>();
+ localPrefixes.add("127.0.0.0/8");
+ localPrefixes.add("fe80::/64");
+ mOffloadHw.setLocalPrefixes(localPrefixes);
+ verify(mIOffloadControl).setLocalPrefixes(eq(localPrefixes), any());
+ }
+
+ @Test
+ public void testSetDataLimit() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ final long limit = 12345;
+ mOffloadHw.setDataLimit(RMNET0, limit);
+ verify(mIOffloadControl).setDataLimit(eq(RMNET0), eq(limit), any());
+ }
+
+ @Test
+ public void testSetDataWarningAndLimit() throws Exception {
+ // Verify V1.0 control HAL would reject the function call with exception.
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ final long warning = 12345;
+ final long limit = 67890;
+ assertThrows(IllegalArgumentException.class,
+ () -> mOffloadHw.setDataWarningAndLimit(RMNET0, warning, limit));
+ reset(mIOffloadControl);
+
+ // Verify V1.1 control HAL could receive this function call.
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_1);
+ mOffloadHw.setDataWarningAndLimit(RMNET0, warning, limit);
+ verify((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mIOffloadControl)
+ .setDataWarningAndLimit(eq(RMNET0), eq(warning), eq(limit), any());
+ }
+
+ @Test
+ public void testSetUpstreamParameters() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ final String v4addr = "192.168.10.1";
+ final String v4gateway = "192.168.10.255";
+ final ArrayList<String> v6gws = new ArrayList<>(0);
+ v6gws.add("2001:db8::1");
+ mOffloadHw.setUpstreamParameters(RMNET0, v4addr, v4gateway, v6gws);
+ verify(mIOffloadControl).setUpstreamParameters(eq(RMNET0), eq(v4addr), eq(v4gateway),
+ eq(v6gws), any());
+
+ final ArgumentCaptor<ArrayList<String>> mArrayListCaptor =
+ ArgumentCaptor.forClass(ArrayList.class);
+ mOffloadHw.setUpstreamParameters(null, null, null, null);
+ verify(mIOffloadControl).setUpstreamParameters(eq(""), eq(""), eq(""),
+ mArrayListCaptor.capture(), any());
+ assertEquals(mArrayListCaptor.getValue().size(), 0);
+ }
+
+ @Test
+ public void testUpdateDownstreamPrefix() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ final String ifName = "wlan1";
+ final String prefix = "192.168.43.0/24";
+ mOffloadHw.addDownstreamPrefix(ifName, prefix);
+ verify(mIOffloadControl).addDownstream(eq(ifName), eq(prefix), any());
+
+ mOffloadHw.removeDownstreamPrefix(ifName, prefix);
+ verify(mIOffloadControl).removeDownstream(eq(ifName), eq(prefix), any());
+ }
+
+ @Test
+ public void testTetheringOffloadCallback() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+
+ mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STARTED);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onStarted();
+
+ mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onStoppedError();
+
+ mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onStoppedUnsupported();
+
+ mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onSupportAvailable();
+
+ mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onStoppedLimitReached();
+
+ final NatTimeoutUpdate tcpParams = buildNatTimeoutUpdate(NetworkProtocol.TCP);
+ mTetheringOffloadCallback.updateTimeout(tcpParams);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onNatTimeoutUpdate(eq(OsConstants.IPPROTO_TCP),
+ eq(tcpParams.src.addr),
+ eq(uint16(tcpParams.src.port)),
+ eq(tcpParams.dst.addr),
+ eq(uint16(tcpParams.dst.port)));
+
+ final NatTimeoutUpdate udpParams = buildNatTimeoutUpdate(NetworkProtocol.UDP);
+ mTetheringOffloadCallback.updateTimeout(udpParams);
+ mTestLooper.dispatchAll();
+ verify(mControlCallback).onNatTimeoutUpdate(eq(OsConstants.IPPROTO_UDP),
+ eq(udpParams.src.addr),
+ eq(uint16(udpParams.src.port)),
+ eq(udpParams.dst.addr),
+ eq(uint16(udpParams.dst.port)));
+ reset(mControlCallback);
+
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_1);
+
+ // Verify the interface will process the events that comes from V1.1 HAL.
+ mTetheringOffloadCallback.onEvent_1_1(OffloadCallbackEvent.OFFLOAD_STARTED);
+ mTestLooper.dispatchAll();
+ final InOrder inOrder = inOrder(mControlCallback);
+ inOrder.verify(mControlCallback).onStarted();
+ inOrder.verifyNoMoreInteractions();
+
+ mTetheringOffloadCallback.onEvent_1_1(OffloadCallbackEvent.OFFLOAD_WARNING_REACHED);
+ mTestLooper.dispatchAll();
+ inOrder.verify(mControlCallback).onWarningReached();
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testSendIpv4NfGenMsg() throws Exception {
+ startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0);
+ FileDescriptor writeSocket = new FileDescriptor();
+ FileDescriptor readSocket = new FileDescriptor();
+ try {
+ Os.socketpair(AF_UNIX, SOCK_STREAM, 0, writeSocket, readSocket);
+ } catch (ErrnoException e) {
+ fail();
+ return;
+ }
+ when(mNativeHandle.getFileDescriptor()).thenReturn(writeSocket);
+
+ mOffloadHw.sendIpv4NfGenMsg(mNativeHandle, TEST_TYPE, TEST_FLAGS);
+
+ ByteBuffer buffer = ByteBuffer.allocate(9823); // Arbitrary value > expectedLen.
+ buffer.order(ByteOrder.nativeOrder());
+
+ int read = Os.read(readSocket, buffer);
+ final int expectedLen = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+ assertEquals(expectedLen, read);
+
+ buffer.flip();
+ assertEquals(expectedLen, buffer.getInt());
+ assertEquals(TEST_TYPE, buffer.getShort());
+ assertEquals(TEST_FLAGS, buffer.getShort());
+ assertEquals(0 /* seq */, buffer.getInt());
+ assertEquals(0 /* pid */, buffer.getInt());
+ assertEquals(AF_INET, buffer.get()); // nfgen_family
+ assertEquals(0 /* error */, buffer.get()); // version
+ assertEquals(0 /* error */, buffer.getShort()); // res_id
+ assertEquals(expectedLen, buffer.position());
+ }
+
+ private NatTimeoutUpdate buildNatTimeoutUpdate(final int proto) {
+ final NatTimeoutUpdate params = new NatTimeoutUpdate();
+ params.proto = proto;
+ params.src.addr = "192.168.43.200";
+ params.src.port = 100;
+ params.dst.addr = "172.50.46.169";
+ params.dst.port = 150;
+ return params;
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
new file mode 100644
index 0000000..41d46e5
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -0,0 +1,551 @@
+/*
+ * 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.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.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.util.PrefixUtils.asIpPrefix;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+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 androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class PrivateAddressCoordinatorTest {
+ private static final String TEST_IFNAME = "test0";
+
+ @Mock private IpServer mHotspotIpServer;
+ @Mock private IpServer mUsbIpServer;
+ @Mock private IpServer mEthernetIpServer;
+ @Mock private IpServer mWifiP2pIpServer;
+ @Mock private Context mContext;
+ @Mock private ConnectivityManager mConnectivityMgr;
+ @Mock private TetheringConfiguration mConfig;
+
+ private PrivateAddressCoordinator mPrivateAddressCoordinator;
+ private final LinkAddress mBluetoothAddress = new LinkAddress("192.168.44.1/24");
+ private final LinkAddress mLegacyWifiP2pAddress = new LinkAddress("192.168.49.1/24");
+ private final Network mWifiNetwork = new Network(1);
+ private final Network mMobileNetwork = new Network(2);
+ private final Network mVpnNetwork = new Network(3);
+ private final Network mMobileNetwork2 = new Network(4);
+ private final Network mMobileNetwork3 = new Network(5);
+ private final Network mMobileNetwork4 = new Network(6);
+ private final Network mMobileNetwork5 = new Network(7);
+ private final Network mMobileNetwork6 = new Network(8);
+ private final Network[] mAllNetworks = {mMobileNetwork, mWifiNetwork, mVpnNetwork,
+ mMobileNetwork2, mMobileNetwork3, mMobileNetwork4, mMobileNetwork5, mMobileNetwork6};
+ private final ArrayList<IpPrefix> mTetheringPrefixes = new ArrayList<>(Arrays.asList(
+ new IpPrefix("192.168.0.0/16"),
+ new IpPrefix("172.16.0.0/12"),
+ new IpPrefix("10.0.0.0/8")));
+
+ private void setUpIpServers() throws Exception {
+ when(mUsbIpServer.interfaceType()).thenReturn(TETHERING_USB);
+ when(mEthernetIpServer.interfaceType()).thenReturn(TETHERING_ETHERNET);
+ when(mHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
+ when(mWifiP2pIpServer.interfaceType()).thenReturn(TETHERING_WIFI_P2P);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
+ when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
+ when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
+ when(mConfig.isSelectAllPrefixRangeEnabled()).thenReturn(true);
+ setUpIpServers();
+ mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+ }
+
+ private LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) {
+ final LinkAddress address = mPrivateAddressCoordinator.requestDownstreamAddress(
+ ipServer, useLastAddress);
+ when(ipServer.getAddress()).thenReturn(address);
+ return address;
+ }
+
+ @Test
+ public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception {
+ final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress);
+ final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
+ false /* useLastAddress */);
+ final IpPrefix hotspotPrefix = asIpPrefix(address);
+ assertNotEquals(hotspotPrefix, bluetoothPrefix);
+
+ final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
+ false /* useLastAddress */);
+ final IpPrefix testDupRequest = asIpPrefix(newAddress);
+ assertNotEquals(hotspotPrefix, testDupRequest);
+ assertNotEquals(bluetoothPrefix, testDupRequest);
+ mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+
+ final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
+ false /* useLastAddress */);
+ final IpPrefix usbPrefix = asIpPrefix(usbAddress);
+ assertNotEquals(usbPrefix, bluetoothPrefix);
+ assertNotEquals(usbPrefix, hotspotPrefix);
+ mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
+ }
+
+ @Test
+ public void testSanitizedAddress() throws Exception {
+ int fakeSubAddr = 0x2b00; // 43.0.
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
+ LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
+ 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, 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, 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, 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(
+ getSubAddress(mBluetoothAddress.getAddress().getAddress()));
+ final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
+ false /* useLastAddress */);
+ final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
+ assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
+ mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+
+ // - Test previous enabled hotspot prefix(cached prefix) is reserved.
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
+ getSubAddress(hotspotAddress.getAddress().getAddress()));
+ final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
+ false /* useLastAddress */);
+ final IpPrefix usbPrefix = asIpPrefix(usbAddress);
+ assertNotEquals(asIpPrefix(mBluetoothAddress), usbPrefix);
+ assertNotEquals(hotspotPrefix, usbPrefix);
+ mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
+
+ // - Test wifi p2p prefix is reserved.
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
+ getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
+ final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer,
+ false /* useLastAddress */);
+ final IpPrefix etherPrefix = asIpPrefix(etherAddress);
+ assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
+ assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
+ assertNotEquals(hotspotPrefix, etherPrefix);
+ mPrivateAddressCoordinator.releaseDownstream(mEthernetIpServer);
+ }
+
+ @Test
+ public void testRequestLastDownstreamAddress() throws Exception {
+ final int fakeHotspotSubAddr = 0x2b05; // 43.5
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
+ final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
+ true /* useLastAddress */);
+ assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+
+ final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
+ 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,
+ true /* useLastAddress */);
+ assertEquals(hotspotAddress, newHotspotAddress);
+ final LinkAddress newUsbAddress = requestDownstreamAddress(mUsbIpServer,
+ true /* useLastAddress */);
+ assertEquals(usbAddress, newUsbAddress);
+
+ final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
+ new LinkAddress("192.168.88.23/16"), null,
+ makeNetworkCapabilities(TRANSPORT_WIFI));
+ mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
+ verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
+ verify(mUsbIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
+ }
+
+ private UpstreamNetworkState buildUpstreamNetworkState(final Network network,
+ final LinkAddress v4Addr, final LinkAddress v6Addr, final NetworkCapabilities cap) {
+ final LinkProperties prop = new LinkProperties();
+ prop.setInterfaceName(TEST_IFNAME);
+ if (v4Addr != null) prop.addLinkAddress(v4Addr);
+
+ if (v6Addr != null) prop.addLinkAddress(v6Addr);
+
+ return new UpstreamNetworkState(prop, cap, network);
+ }
+
+ private NetworkCapabilities makeNetworkCapabilities(final int transportType) {
+ final NetworkCapabilities cap = new NetworkCapabilities();
+ cap.addTransportType(transportType);
+ if (transportType == TRANSPORT_VPN) {
+ cap.removeCapability(NET_CAPABILITY_NOT_VPN);
+ }
+
+ return cap;
+ }
+
+ @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,
+ 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,
+ 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,
+ 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,
+ true /* useLastAddress */);
+ final IpPrefix ethPrefix = asIpPrefix(ethAddr);
+ assertEquals(predefinedPrefix, ethPrefix);
+ }
+
+ @Test
+ public void testChooseAvailablePrefix() throws Exception {
+ final int randomAddress = 0x8605; // 134.5
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
+ final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ 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,
+ true /* useLastAddress */);
+ assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
+ }
+
+ private void verifyNotifyConflictAndRelease(final IpServer ipServer) throws Exception {
+ verify(ipServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
+ mPrivateAddressCoordinator.releaseDownstream(ipServer);
+ reset(ipServer);
+ setUpIpServers();
+ }
+
+ private int getSubAddress(final byte... ipv4Address) {
+ assertEquals(4, ipv4Address.length);
+
+ int subnet = Byte.toUnsignedInt(ipv4Address[2]);
+ return (subnet << 8) + ipv4Address[3];
+ }
+
+ private void assertReseveredWifiP2pPrefix() throws Exception {
+ LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
+ true /* useLastAddress */);
+ final IpPrefix hotspotPrefix = asIpPrefix(address);
+ final IpPrefix legacyWifiP2pPrefix = asIpPrefix(mLegacyWifiP2pAddress);
+ assertNotEquals(legacyWifiP2pPrefix, hotspotPrefix);
+ mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+ }
+
+ @Test
+ public void testEnableLegacyWifiP2PAddress() throws Exception {
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
+ getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
+ // No matter #shouldEnableWifiP2pDedicatedIp() is enabled or not, legacy wifi p2p prefix
+ // is resevered.
+ assertReseveredWifiP2pPrefix();
+
+ when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(true);
+ assertReseveredWifiP2pPrefix();
+
+ // If #shouldEnableWifiP2pDedicatedIp() is enabled, wifi P2P gets the configured address.
+ LinkAddress address = requestDownstreamAddress(mWifiP2pIpServer,
+ true /* useLastAddress */);
+ assertEquals(mLegacyWifiP2pAddress, address);
+ mPrivateAddressCoordinator.releaseDownstream(mWifiP2pIpServer);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
new file mode 100644
index 0000000..6090213
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -0,0 +1,403 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import androidx.annotation.Nullable;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts.
+ *
+ * Unlike any real networking code, this class is single-threaded and entirely synchronous.
+ * The effects of all method calls (including sending fake broadcasts, sending callbacks, etc.) are
+ * performed immediately on the caller's thread before returning.
+ *
+ * TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService.
+ * Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest.
+ *
+ * Things to consider:
+ * - ConnectivityService uses a real handler for realism, and these test use TestLooper (or even
+ * invoke callbacks directly inline) for determinism. Using a real ConnectivityService would
+ * require adding dispatchAll() calls and migrating to handlers.
+ * - ConnectivityService does not provide a way to order CONNECTIVITY_ACTION before or after the
+ * NetworkCallbacks for the same network change. That ability is useful because the upstream
+ * selection code in Tethering is vulnerable to race conditions, due to its reliance on multiple
+ * separate NetworkCallbacks and BroadcastReceivers, each of which trigger different types of
+ * updates. If/when the upstream selection code is refactored to a more level-triggered model
+ * (e.g., with an idempotent function that takes into account all state every time any part of
+ * that state changes), this may become less important or unnecessary.
+ */
+public class TestConnectivityManager extends ConnectivityManager {
+ public static final boolean BROADCAST_FIRST = false;
+ public static final boolean CALLBACKS_FIRST = true;
+
+ final Map<NetworkCallback, NetworkRequestInfo> mAllCallbacks = new ArrayMap<>();
+ // This contains the callbacks tracking the system default network, whether it's registered
+ // with registerSystemDefaultNetworkCallback (S+) or with a custom request (R-).
+ final Map<NetworkCallback, NetworkRequestInfo> mTrackingDefault = new ArrayMap<>();
+ final Map<NetworkCallback, NetworkRequestInfo> mListening = new ArrayMap<>();
+ final Map<NetworkCallback, NetworkRequestInfo> mRequested = new ArrayMap<>();
+ final Map<NetworkCallback, Integer> mLegacyTypeMap = new ArrayMap<>();
+
+ private final Context mContext;
+
+ private int mNetworkId = 100;
+ private TestNetworkAgent mDefaultNetwork = null;
+
+ /**
+ * Constructs a TestConnectivityManager.
+ * @param ctx the context to use. Must be a fake or a mock because otherwise the test will
+ * attempt to send real broadcasts and resulting in permission denials.
+ * @param svc an IConnectivityManager. Should be a fake or a mock.
+ */
+ public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
+ super(ctx, svc);
+ mContext = ctx;
+ }
+
+ class NetworkRequestInfo {
+ public final NetworkRequest request;
+ public final Handler handler;
+ NetworkRequestInfo(NetworkRequest r, Handler h) {
+ request = r;
+ handler = h;
+ }
+ }
+
+ boolean hasNoCallbacks() {
+ return mAllCallbacks.isEmpty()
+ && mTrackingDefault.isEmpty()
+ && mListening.isEmpty()
+ && mRequested.isEmpty()
+ && mLegacyTypeMap.isEmpty();
+ }
+
+ boolean onlyHasDefaultCallbacks() {
+ return (mAllCallbacks.size() == 1)
+ && (mTrackingDefault.size() == 1)
+ && mListening.isEmpty()
+ && mRequested.isEmpty()
+ && mLegacyTypeMap.isEmpty();
+ }
+
+ boolean isListeningForAll() {
+ final NetworkCapabilities empty = new NetworkCapabilities();
+ empty.clearAll();
+
+ for (NetworkRequestInfo nri : mListening.values()) {
+ if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ int getNetworkId() {
+ return ++mNetworkId;
+ }
+
+ private void sendDefaultNetworkBroadcasts(TestNetworkAgent formerDefault,
+ TestNetworkAgent defaultNetwork) {
+ if (formerDefault != null) {
+ sendConnectivityAction(formerDefault.legacyType, false /* connected */);
+ }
+ if (defaultNetwork != null) {
+ sendConnectivityAction(defaultNetwork.legacyType, true /* connected */);
+ }
+ }
+
+ private void sendDefaultNetworkCallbacks(TestNetworkAgent formerDefault,
+ TestNetworkAgent defaultNetwork) {
+ for (NetworkCallback cb : mTrackingDefault.keySet()) {
+ final NetworkRequestInfo nri = mTrackingDefault.get(cb);
+ if (defaultNetwork != null) {
+ nri.handler.post(() -> cb.onAvailable(defaultNetwork.networkId));
+ nri.handler.post(() -> cb.onCapabilitiesChanged(
+ defaultNetwork.networkId, defaultNetwork.networkCapabilities));
+ nri.handler.post(() -> cb.onLinkPropertiesChanged(
+ defaultNetwork.networkId, defaultNetwork.linkProperties));
+ } else if (formerDefault != null) {
+ nri.handler.post(() -> cb.onLost(formerDefault.networkId));
+ }
+ }
+ }
+
+ void makeDefaultNetwork(TestNetworkAgent agent, boolean order, @Nullable Runnable inBetween) {
+ if (Objects.equals(mDefaultNetwork, agent)) return;
+
+ final TestNetworkAgent formerDefault = mDefaultNetwork;
+ mDefaultNetwork = agent;
+
+ if (order == CALLBACKS_FIRST) {
+ sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork);
+ if (inBetween != null) inBetween.run();
+ sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork);
+ } else {
+ sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork);
+ if (inBetween != null) inBetween.run();
+ sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork);
+ }
+ }
+
+ void makeDefaultNetwork(TestNetworkAgent agent, boolean order) {
+ makeDefaultNetwork(agent, order, null /* inBetween */);
+ }
+
+ void makeDefaultNetwork(TestNetworkAgent agent) {
+ makeDefaultNetwork(agent, BROADCAST_FIRST, null /* inBetween */);
+ }
+
+ static boolean looksLikeDefaultRequest(NetworkRequest req) {
+ return req.hasCapability(NET_CAPABILITY_INTERNET)
+ && !req.hasCapability(NET_CAPABILITY_DUN)
+ && !req.hasTransport(TRANSPORT_CELLULAR);
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
+ assertFalse(mAllCallbacks.containsKey(cb));
+ mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ // For R- devices, Tethering will invoke this function in 2 cases, one is to request mobile
+ // network, the other is to track system default network.
+ if (looksLikeDefaultRequest(req)) {
+ assertFalse(mTrackingDefault.containsKey(cb));
+ mTrackingDefault.put(cb, new NetworkRequestInfo(req, h));
+ } else {
+ assertFalse(mRequested.containsKey(cb));
+ mRequested.put(cb, new NetworkRequestInfo(req, h));
+ }
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req,
+ int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
+ assertFalse(mAllCallbacks.containsKey(cb));
+ NetworkRequest newReq = new NetworkRequest(req.networkCapabilities, legacyType,
+ -1 /** testId */, req.type);
+ mAllCallbacks.put(cb, new NetworkRequestInfo(newReq, h));
+ assertFalse(mRequested.containsKey(cb));
+ mRequested.put(cb, new NetworkRequestInfo(newReq, h));
+ assertFalse(mLegacyTypeMap.containsKey(cb));
+ if (legacyType != ConnectivityManager.TYPE_NONE) {
+ mLegacyTypeMap.put(cb, legacyType);
+ }
+ }
+
+ @Override
+ public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
+ assertFalse(mAllCallbacks.containsKey(cb));
+ mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ assertFalse(mListening.containsKey(cb));
+ mListening.put(cb, new NetworkRequestInfo(req, h));
+ }
+
+ @Override
+ public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void registerDefaultNetworkCallback(NetworkCallback cb) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void unregisterNetworkCallback(NetworkCallback cb) {
+ if (mTrackingDefault.containsKey(cb)) {
+ mTrackingDefault.remove(cb);
+ } else if (mListening.containsKey(cb)) {
+ mListening.remove(cb);
+ } else if (mRequested.containsKey(cb)) {
+ mRequested.remove(cb);
+ mLegacyTypeMap.remove(cb);
+ } else {
+ fail("Unexpected callback removed");
+ }
+ mAllCallbacks.remove(cb);
+
+ assertFalse(mAllCallbacks.containsKey(cb));
+ assertFalse(mTrackingDefault.containsKey(cb));
+ assertFalse(mListening.containsKey(cb));
+ assertFalse(mRequested.containsKey(cb));
+ }
+
+ private void sendConnectivityAction(int type, boolean connected) {
+ NetworkInfo ni = new NetworkInfo(type, 0 /* subtype */, getNetworkTypeName(type),
+ "" /* subtypeName */);
+ NetworkInfo.DetailedState state = connected
+ ? NetworkInfo.DetailedState.CONNECTED
+ : NetworkInfo.DetailedState.DISCONNECTED;
+ ni.setDetailedState(state, "" /* reason */, "" /* extraInfo */);
+ Intent intent = new Intent(CONNECTIVITY_ACTION);
+ intent.putExtra(EXTRA_NETWORK_INFO, ni);
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ public static class TestNetworkAgent {
+ public final TestConnectivityManager cm;
+ public final Network networkId;
+ public final NetworkCapabilities networkCapabilities;
+ public final LinkProperties linkProperties;
+ // TODO: delete when tethering no longer uses CONNECTIVITY_ACTION.
+ public final int legacyType;
+
+ public TestNetworkAgent(TestConnectivityManager cm, NetworkCapabilities nc) {
+ this.cm = cm;
+ this.networkId = new Network(cm.getNetworkId());
+ networkCapabilities = copy(nc);
+ linkProperties = new LinkProperties();
+ legacyType = toLegacyType(nc);
+ }
+
+ public TestNetworkAgent(TestConnectivityManager cm, UpstreamNetworkState state) {
+ this.cm = cm;
+ networkId = state.network;
+ networkCapabilities = state.networkCapabilities;
+ linkProperties = state.linkProperties;
+ this.legacyType = toLegacyType(networkCapabilities);
+ }
+
+ private static int toLegacyType(NetworkCapabilities nc) {
+ for (int type = 0; type < ConnectivityManager.TYPE_TEST; type++) {
+ if (matchesLegacyType(nc, type)) return type;
+ }
+ throw new IllegalArgumentException(("Can't determine legacy type for: ") + nc);
+ }
+
+ private static boolean matchesLegacyType(NetworkCapabilities nc, int legacyType) {
+ final NetworkCapabilities typeNc;
+ try {
+ typeNc = ConnectivityManager.networkCapabilitiesForType(legacyType);
+ } catch (IllegalArgumentException e) {
+ // networkCapabilitiesForType does not support all legacy types.
+ return false;
+ }
+ return typeNc.satisfiedByNetworkCapabilities(nc);
+ }
+
+ private boolean matchesLegacyType(int legacyType) {
+ return matchesLegacyType(networkCapabilities, legacyType);
+ }
+
+ private void maybeSendConnectivityBroadcast(boolean connected) {
+ for (Integer requestedLegacyType : cm.mLegacyTypeMap.values()) {
+ if (requestedLegacyType.intValue() == legacyType) {
+ cm.sendConnectivityAction(legacyType, connected /* connected */);
+ // In practice, a given network can match only one legacy type.
+ break;
+ }
+ }
+ }
+
+ public void fakeConnect() {
+ fakeConnect(BROADCAST_FIRST, null);
+ }
+
+ public void fakeConnect(boolean order, @Nullable Runnable inBetween) {
+ if (order == BROADCAST_FIRST) {
+ maybeSendConnectivityBroadcast(true /* connected */);
+ if (inBetween != null) inBetween.run();
+ }
+
+ for (NetworkCallback cb : cm.mListening.keySet()) {
+ final NetworkRequestInfo nri = cm.mListening.get(cb);
+ nri.handler.post(() -> cb.onAvailable(networkId));
+ nri.handler.post(() -> cb.onCapabilitiesChanged(
+ networkId, copy(networkCapabilities)));
+ nri.handler.post(() -> cb.onLinkPropertiesChanged(networkId, copy(linkProperties)));
+ }
+
+ if (order == CALLBACKS_FIRST) {
+ if (inBetween != null) inBetween.run();
+ maybeSendConnectivityBroadcast(true /* connected */);
+ }
+ // mTrackingDefault will be updated if/when the caller calls makeDefaultNetwork
+ }
+
+ public void fakeDisconnect() {
+ fakeDisconnect(BROADCAST_FIRST, null);
+ }
+
+ public void fakeDisconnect(boolean order, @Nullable Runnable inBetween) {
+ if (order == BROADCAST_FIRST) {
+ maybeSendConnectivityBroadcast(false /* connected */);
+ if (inBetween != null) inBetween.run();
+ }
+
+ for (NetworkCallback cb : cm.mListening.keySet()) {
+ cb.onLost(networkId);
+ }
+
+ if (order == CALLBACKS_FIRST) {
+ if (inBetween != null) inBetween.run();
+ maybeSendConnectivityBroadcast(false /* connected */);
+ }
+ // mTrackingDefault will be updated if/when the caller calls makeDefaultNetwork
+ }
+
+ public void sendLinkProperties() {
+ for (NetworkCallback cb : cm.mListening.keySet()) {
+ cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
+ }
+ }
+
+ static NetworkCapabilities copy(NetworkCapabilities nc) {
+ return new NetworkCapabilities(nc);
+ }
+
+ static LinkProperties copy(LinkProperties lp) {
+ return new LinkProperties(lp);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
new file mode 100644
index 0000000..a6433a6
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.util.SharedLog;
+import android.os.Build;
+import android.provider.DeviceConfig;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+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.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TetheringConfigurationTest {
+ private final SharedLog mLog = new SharedLog("TetheringConfigurationTest");
+
+ @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+ private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
+ private static final String PROVISIONING_APP_RESPONSE = "app_response";
+ private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
+ private static final String APEX_NAME = "com.android.tethering";
+ private static final long TEST_PACKAGE_VERSION = 1234L;
+ @Mock private Context mContext;
+ @Mock private TelephonyManager mTelephonyManager;
+ @Mock private Resources mResources;
+ @Mock private Resources mResourcesForSubId;
+ @Mock private PackageManager mPackageManager;
+ @Mock private ModuleInfo mMi;
+ private Context mMockContext;
+ private boolean mHasTelephonyManager;
+ private boolean mEnableLegacyDhcpServer;
+ private MockitoSession mMockingSession;
+
+ private class MockTetheringConfiguration extends TetheringConfiguration {
+ MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
+ super(ctx, log, id);
+ }
+
+ @Override
+ protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
+ return mResourcesForSubId;
+ }
+ }
+
+ private class MockContext extends BroadcastInterceptingContext {
+ MockContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.TELEPHONY_SERVICE.equals(name)) {
+ return mHasTelephonyManager ? mTelephonyManager : null;
+ }
+ return super.getSystemService(name);
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public String getPackageName() {
+ return TEST_PACKAGE_NAME;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // TODO: use a dependencies class instead of mock statics.
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .mockStatic(DeviceConfig.class)
+ .strictness(Strictness.WARN)
+ .startMocking();
+ DeviceConfigUtils.resetPackageVersionCacheForTest();
+ doReturn(null).when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+ setTetherForceUpstreamAutomaticFlagVersion(null);
+
+ final PackageInfo pi = new PackageInfo();
+ pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+ doReturn(pi).when(mPackageManager).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
+ doReturn(mMi).when(mPackageManager).getModuleInfo(eq(APEX_NAME), anyInt());
+ doReturn(TEST_PACKAGE_NAME).when(mMi).getPackageName();
+
+ when(mResources.getStringArray(R.array.config_tether_dhcp_range)).thenReturn(
+ new String[0]);
+ when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn(
+ TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+ when(mResources.getStringArray(R.array.config_tether_usb_regexs)).thenReturn(new String[0]);
+ when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
+ .thenReturn(new String[]{ "test_wlan\\d" });
+ when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs)).thenReturn(
+ new String[0]);
+ when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(new int[0]);
+ when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+ .thenReturn(new String[0]);
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ false);
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip))
+ .thenReturn(false);
+ initializeBpfOffloadConfiguration(true, null /* unset */);
+ initEnableSelectAllPrefixRangeFlag(null /* unset */);
+
+ mHasTelephonyManager = true;
+ mMockContext = new MockContext(mContext);
+ mEnableLegacyDhcpServer = false;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mMockingSession.finishMocking();
+ DeviceConfigUtils.resetPackageVersionCacheForTest();
+ }
+
+ private TetheringConfiguration getTetheringConfiguration(int... legacyTetherUpstreamTypes) {
+ when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
+ legacyTetherUpstreamTypes);
+ return new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ }
+
+ @Test
+ public void testNoTelephonyManagerMeansNoDun() {
+ mHasTelephonyManager = false;
+ final TetheringConfiguration cfg = getTetheringConfiguration(
+ new int[]{TYPE_MOBILE_DUN, TYPE_WIFI});
+ assertFalse(cfg.isDunRequired);
+ assertFalse(cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_DUN));
+ // Just to prove we haven't clobbered Wi-Fi:
+ assertTrue(cfg.preferredUpstreamIfaceTypes.contains(TYPE_WIFI));
+ }
+
+ @Test
+ public void testDunFromTelephonyManagerMeansDun() {
+ when(mTelephonyManager.isTetheringApnRequired()).thenReturn(true);
+
+ final TetheringConfiguration cfgWifi = getTetheringConfiguration(TYPE_WIFI);
+ final TetheringConfiguration cfgMobileWifiHipri = getTetheringConfiguration(
+ TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI);
+ final TetheringConfiguration cfgWifiDun = getTetheringConfiguration(
+ TYPE_WIFI, TYPE_MOBILE_DUN);
+ final TetheringConfiguration cfgMobileWifiHipriDun = getTetheringConfiguration(
+ TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI, TYPE_MOBILE_DUN);
+
+ for (TetheringConfiguration cfg : Arrays.asList(cfgWifi, cfgMobileWifiHipri,
+ cfgWifiDun, cfgMobileWifiHipriDun)) {
+ String msg = "config=" + cfg.toString();
+ assertTrue(msg, cfg.isDunRequired);
+ assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_DUN));
+ assertFalse(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE));
+ assertFalse(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI));
+ // Just to prove we haven't clobbered Wi-Fi:
+ assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_WIFI));
+ }
+ }
+
+ @Test
+ public void testDunNotRequiredFromTelephonyManagerMeansNoDun() {
+ when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
+
+ final TetheringConfiguration cfgWifi = getTetheringConfiguration(TYPE_WIFI);
+ final TetheringConfiguration cfgMobileWifiHipri = getTetheringConfiguration(
+ TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI);
+ final TetheringConfiguration cfgWifiDun = getTetheringConfiguration(
+ TYPE_WIFI, TYPE_MOBILE_DUN);
+ final TetheringConfiguration cfgWifiMobile = getTetheringConfiguration(
+ TYPE_WIFI, TYPE_MOBILE);
+ final TetheringConfiguration cfgWifiHipri = getTetheringConfiguration(
+ TYPE_WIFI, TYPE_MOBILE_HIPRI);
+ final TetheringConfiguration cfgMobileWifiHipriDun = getTetheringConfiguration(
+ TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI, TYPE_MOBILE_DUN);
+
+ String msg;
+ // TYPE_MOBILE_DUN should be present in none of the combinations.
+ // TYPE_WIFI should not be affected.
+ for (TetheringConfiguration cfg : Arrays.asList(cfgWifi, cfgMobileWifiHipri, cfgWifiDun,
+ cfgWifiMobile, cfgWifiHipri, cfgMobileWifiHipriDun)) {
+ msg = "config=" + cfg.toString();
+ assertFalse(msg, cfg.isDunRequired);
+ assertFalse(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_DUN));
+ assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_WIFI));
+ }
+
+ for (TetheringConfiguration cfg : Arrays.asList(cfgWifi, cfgMobileWifiHipri, cfgWifiDun,
+ cfgMobileWifiHipriDun)) {
+ msg = "config=" + cfg.toString();
+ assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE));
+ assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI));
+ }
+ msg = "config=" + cfgWifiMobile.toString();
+ assertTrue(msg, cfgWifiMobile.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE));
+ assertFalse(msg, cfgWifiMobile.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI));
+ msg = "config=" + cfgWifiHipri.toString();
+ assertFalse(msg, cfgWifiHipri.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE));
+ assertTrue(msg, cfgWifiHipri.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI));
+
+ }
+
+ @Test
+ public void testNoDefinedUpstreamTypesAddsEthernet() {
+ when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(new int[]{});
+ when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
+
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ final Iterator<Integer> upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator();
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue());
+ // The following is because the code always adds some kind of mobile
+ // upstream, be it DUN or, in this case where DUN is NOT required,
+ // make sure there is at least one of MOBILE or HIPRI. With the empty
+ // list of the configuration in this test, it will always add both
+ // MOBILE and HIPRI, in that order.
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_MOBILE, upstreamIterator.next().intValue());
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_MOBILE_HIPRI, upstreamIterator.next().intValue());
+ assertFalse(upstreamIterator.hasNext());
+ }
+
+ @Test
+ public void testDefinedUpstreamTypesSansEthernetAddsEthernet() {
+ when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
+ new int[]{TYPE_WIFI, TYPE_MOBILE_HIPRI});
+ when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
+
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ final Iterator<Integer> upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator();
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue());
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_WIFI, upstreamIterator.next().intValue());
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_MOBILE_HIPRI, upstreamIterator.next().intValue());
+ assertFalse(upstreamIterator.hasNext());
+ }
+
+ @Test
+ public void testDefinedUpstreamTypesWithEthernetDoesNotAddEthernet() {
+ when(mResources.getIntArray(R.array.config_tether_upstream_types))
+ .thenReturn(new int[]{TYPE_WIFI, TYPE_ETHERNET, TYPE_MOBILE_HIPRI});
+ when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
+
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ final Iterator<Integer> upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator();
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_WIFI, upstreamIterator.next().intValue());
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue());
+ assertTrue(upstreamIterator.hasNext());
+ assertEquals(TYPE_MOBILE_HIPRI, upstreamIterator.next().intValue());
+ assertFalse(upstreamIterator.hasNext());
+ }
+
+ private void initializeBpfOffloadConfiguration(
+ final boolean fromRes, final String fromDevConfig) {
+ when(mResources.getBoolean(R.bool.config_tether_enable_bpf_offload)).thenReturn(fromRes);
+ doReturn(fromDevConfig).when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD)));
+ }
+
+ @Test
+ public void testBpfOffloadEnabledByResource() {
+ initializeBpfOffloadConfiguration(true, null /* unset */);
+ final TetheringConfiguration enableByRes =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(enableByRes.isBpfOffloadEnabled());
+ }
+
+ @Test
+ public void testBpfOffloadEnabledByDeviceConfigOverride() {
+ for (boolean res : new boolean[]{true, false}) {
+ initializeBpfOffloadConfiguration(res, "true");
+ final TetheringConfiguration enableByDevConOverride =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(enableByDevConOverride.isBpfOffloadEnabled());
+ }
+ }
+
+ @Test
+ public void testBpfOffloadDisabledByResource() {
+ initializeBpfOffloadConfiguration(false, null /* unset */);
+ final TetheringConfiguration disableByRes =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertFalse(disableByRes.isBpfOffloadEnabled());
+ }
+
+ @Test
+ public void testBpfOffloadDisabledByDeviceConfigOverride() {
+ for (boolean res : new boolean[]{true, false}) {
+ initializeBpfOffloadConfiguration(res, "false");
+ final TetheringConfiguration disableByDevConOverride =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertFalse(disableByDevConOverride.isBpfOffloadEnabled());
+ }
+ }
+
+ @Test
+ public void testNewDhcpServerDisabled() {
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ true);
+ doReturn("false").when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+
+ final TetheringConfiguration enableByRes =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(enableByRes.enableLegacyDhcpServer);
+
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ false);
+ doReturn("true").when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+
+ final TetheringConfiguration enableByDevConfig =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(enableByDevConfig.enableLegacyDhcpServer);
+ }
+
+ @Test
+ public void testNewDhcpServerEnabled() {
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ false);
+ doReturn("false").when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+
+ final TetheringConfiguration cfg =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+ assertFalse(cfg.enableLegacyDhcpServer);
+ }
+
+ @Test
+ public void testOffloadIntervalByResource() {
+ final TetheringConfiguration intervalByDefault =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertEquals(TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS,
+ intervalByDefault.getOffloadPollInterval());
+
+ final int[] testOverrides = {0, 3000, -1};
+ for (final int override : testOverrides) {
+ when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn(
+ override);
+ final TetheringConfiguration overrideByRes =
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertEquals(override, overrideByRes.getOffloadPollInterval());
+ }
+ }
+
+ @Test
+ public void testGetResourcesBySubId() {
+ setUpResourceForSubId();
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(cfg.provisioningApp.length == 0);
+ final int anyValidSubId = 1;
+ final MockTetheringConfiguration mockCfg =
+ new MockTetheringConfiguration(mMockContext, mLog, anyValidSubId);
+ assertEquals(mockCfg.provisioningApp[0], PROVISIONING_APP_NAME[0]);
+ assertEquals(mockCfg.provisioningApp[1], PROVISIONING_APP_NAME[1]);
+ assertEquals(mockCfg.provisioningAppNoUi, PROVISIONING_NO_UI_APP_NAME);
+ assertEquals(mockCfg.provisioningResponse, PROVISIONING_APP_RESPONSE);
+ }
+
+ private void setUpResourceForSubId() {
+ when(mResourcesForSubId.getStringArray(
+ R.array.config_tether_dhcp_range)).thenReturn(new String[0]);
+ when(mResourcesForSubId.getStringArray(
+ R.array.config_tether_usb_regexs)).thenReturn(new String[0]);
+ when(mResourcesForSubId.getStringArray(
+ R.array.config_tether_wifi_regexs)).thenReturn(new String[]{ "test_wlan\\d" });
+ when(mResourcesForSubId.getStringArray(
+ R.array.config_tether_bluetooth_regexs)).thenReturn(new String[0]);
+ when(mResourcesForSubId.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
+ new int[0]);
+ when(mResourcesForSubId.getStringArray(
+ R.array.config_mobile_hotspot_provision_app)).thenReturn(PROVISIONING_APP_NAME);
+ when(mResourcesForSubId.getString(R.string.config_mobile_hotspot_provision_app_no_ui))
+ .thenReturn(PROVISIONING_NO_UI_APP_NAME);
+ when(mResourcesForSubId.getString(
+ R.string.config_mobile_hotspot_provision_response)).thenReturn(
+ PROVISIONING_APP_RESPONSE);
+ }
+
+ @Test
+ public void testEnableLegacyWifiP2PAddress() throws Exception {
+ final TetheringConfiguration defaultCfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertFalse(defaultCfg.shouldEnableWifiP2pDedicatedIp());
+
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip))
+ .thenReturn(true);
+ final TetheringConfiguration testCfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(testCfg.shouldEnableWifiP2pDedicatedIp());
+ }
+
+ private void initEnableSelectAllPrefixRangeFlag(final String value) {
+ doReturn(value).when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES)));
+ }
+
+ @Test
+ public void testSelectAllPrefixRangeFlag() throws Exception {
+ // Test default value.
+ final TetheringConfiguration defaultCfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(defaultCfg.isSelectAllPrefixRangeEnabled());
+
+ // Test disable flag.
+ initEnableSelectAllPrefixRangeFlag("false");
+ final TetheringConfiguration testDisable = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertFalse(testDisable.isSelectAllPrefixRangeEnabled());
+
+ // Test enable flag.
+ initEnableSelectAllPrefixRangeFlag("true");
+ final TetheringConfiguration testEnable = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ assertTrue(testEnable.isSelectAllPrefixRangeEnabled());
+ }
+
+ @Test
+ public void testChooseUpstreamAutomatically() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(true);
+ assertChooseUpstreamAutomaticallyIs(true);
+
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ assertChooseUpstreamAutomaticallyIs(false);
+ }
+
+ // The flag override only works on R-
+ @Test @IgnoreAfter(Build.VERSION_CODES.R)
+ public void testChooseUpstreamAutomatically_FlagOverride() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+ assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY,
+ TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION, APEX_NAME, false));
+
+ assertChooseUpstreamAutomaticallyIs(true);
+
+ setTetherForceUpstreamAutomaticFlagVersion(0L);
+ assertChooseUpstreamAutomaticallyIs(false);
+
+ setTetherForceUpstreamAutomaticFlagVersion(Long.MAX_VALUE);
+ assertChooseUpstreamAutomaticallyIs(false);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testChooseUpstreamAutomatically_FlagOverrideAfterR() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+ assertChooseUpstreamAutomaticallyIs(false);
+ }
+
+ private void setTetherForceUpstreamAutomaticFlagVersion(Long version) {
+ doReturn(version == null ? null : Long.toString(version)).when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION)));
+ }
+
+ private void assertChooseUpstreamAutomaticallyIs(boolean value) {
+ assertEquals(value, new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID)
+ .chooseUpstreamAutomatically);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
new file mode 100644
index 0000000..75c819b
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
@@ -0,0 +1,444 @@
+/*
+ * 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.networkstack.tethering
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.res.Resources
+import android.net.ConnectivityManager.TETHERING_WIFI
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.UserHandle
+import android.provider.Settings
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.internal.util.test.BroadcastInterceptingContext
+import com.android.networkstack.tethering.TetheringNotificationUpdater.ACTION_DISABLE_TETHERING
+import com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE
+import com.android.networkstack.tethering.TetheringNotificationUpdater.EVENT_SHOW_NO_UPSTREAM
+import com.android.networkstack.tethering.TetheringNotificationUpdater.NO_UPSTREAM_NOTIFICATION_ID
+import com.android.networkstack.tethering.TetheringNotificationUpdater.RESTRICTED_NOTIFICATION_ID
+import com.android.networkstack.tethering.TetheringNotificationUpdater.ROAMING_NOTIFICATION_ID
+import com.android.networkstack.tethering.TetheringNotificationUpdater.VERIZON_CARRIER_ID
+import com.android.networkstack.tethering.TetheringNotificationUpdater.getSettingsPackageName
+import com.android.testutils.waitForIdle
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+const val TEST_SUBID = 1
+const val WIFI_MASK = 1 shl TETHERING_WIFI
+const val TEST_DISALLOW_TITLE = "Tether function is disallowed"
+const val TEST_DISALLOW_MESSAGE = "Please contact your admin"
+const val TEST_NO_UPSTREAM_TITLE = "Hotspot has no internet access"
+const val TEST_NO_UPSTREAM_MESSAGE = "Device cannot connect to internet."
+const val TEST_NO_UPSTREAM_BUTTON = "Turn off hotspot"
+const val TEST_ROAMING_TITLE = "Hotspot is on"
+const val TEST_ROAMING_MESSAGE = "Additional charges may apply while roaming."
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TetheringNotificationUpdaterTest {
+ // lateinit used here for mocks as they need to be reinitialized between each test and the test
+ // should crash if they are used before being initialized.
+ @Mock private lateinit var mockContext: Context
+ @Mock private lateinit var notificationManager: NotificationManager
+ @Mock private lateinit var telephonyManager: TelephonyManager
+ @Mock private lateinit var testResources: Resources
+
+ // lateinit for these classes under test, as they should be reset to a different instance for
+ // every test but should always be initialized before use (or the test should crash).
+ private lateinit var context: TestContext
+ private lateinit var notificationUpdater: TetheringNotificationUpdater
+
+ // Initializing the following members depends on initializing some of the mocks and
+ // is more logically done in setup().
+ private lateinit var fakeTetheringThread: HandlerThread
+
+ private val ROAMING_CAPABILITIES = NetworkCapabilities()
+ private val HOME_CAPABILITIES = NetworkCapabilities().addCapability(NET_CAPABILITY_NOT_ROAMING)
+ private val NOTIFICATION_ICON_ID = R.drawable.stat_sys_tether_general
+ private val TIMEOUT_MS = 500L
+ private val ACTIVITY_PENDING_INTENT = 0
+ private val BROADCAST_PENDING_INTENT = 1
+
+ private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) {
+ override fun createContextAsUser(user: UserHandle, flags: Int) =
+ if (user == UserHandle.ALL) mockContext else this
+ override fun getSystemService(name: String) =
+ if (name == Context.TELEPHONY_SERVICE) telephonyManager
+ else super.getSystemService(name)
+ }
+
+ private inner class WrappedNotificationUpdater(c: Context, looper: Looper)
+ : TetheringNotificationUpdater(c, looper) {
+ override fun getResourcesForSubId(c: Context, subId: Int) =
+ if (subId == TEST_SUBID) testResources else super.getResourcesForSubId(c, subId)
+ }
+
+ private fun setupResources() {
+ doReturn(5).`when`(testResources)
+ .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
+ doReturn(true).`when`(testResources)
+ .getBoolean(R.bool.config_upstream_roaming_notification)
+ doReturn(TEST_DISALLOW_TITLE).`when`(testResources)
+ .getString(R.string.disable_tether_notification_title)
+ doReturn(TEST_DISALLOW_MESSAGE).`when`(testResources)
+ .getString(R.string.disable_tether_notification_message)
+ doReturn(TEST_NO_UPSTREAM_TITLE).`when`(testResources)
+ .getString(R.string.no_upstream_notification_title)
+ doReturn(TEST_NO_UPSTREAM_MESSAGE).`when`(testResources)
+ .getString(R.string.no_upstream_notification_message)
+ doReturn(TEST_NO_UPSTREAM_BUTTON).`when`(testResources)
+ .getString(R.string.no_upstream_notification_disable_button)
+ doReturn(TEST_ROAMING_TITLE).`when`(testResources)
+ .getString(R.string.upstream_roaming_notification_title)
+ doReturn(TEST_ROAMING_MESSAGE).`when`(testResources)
+ .getString(R.string.upstream_roaming_notification_message)
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context = TestContext(InstrumentationRegistry.getInstrumentation().context)
+ doReturn(notificationManager).`when`(mockContext)
+ .getSystemService(Context.NOTIFICATION_SERVICE)
+ fakeTetheringThread = HandlerThread(this::class.java.simpleName)
+ fakeTetheringThread.start()
+ notificationUpdater = WrappedNotificationUpdater(context, fakeTetheringThread.looper)
+ setupResources()
+ }
+
+ @After
+ fun tearDown() {
+ fakeTetheringThread.quitSafely()
+ }
+
+ private fun verifyActivityPendingIntent(intent: Intent, flags: Int) {
+ // Use FLAG_NO_CREATE to verify whether PendingIntent has FLAG_IMMUTABLE flag(forcefully add
+ // the flag in creating arguments). If the described PendingIntent does not already exist,
+ // getActivity() will return null instead of PendingIntent object.
+ val pi = PendingIntent.getActivity(
+ context.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ intent,
+ flags or FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE,
+ null /* options */)
+ assertNotNull("Activity PendingIntent with FLAG_IMMUTABLE does not exist.", pi)
+ }
+
+ private fun verifyBroadcastPendingIntent(intent: Intent, flags: Int) {
+ // Use FLAG_NO_CREATE to verify whether PendingIntent has FLAG_IMMUTABLE flag(forcefully add
+ // the flag in creating arguments). If the described PendingIntent does not already exist,
+ // getBroadcast() will return null instead of PendingIntent object.
+ val pi = PendingIntent.getBroadcast(
+ context.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ intent,
+ flags or FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE)
+ assertNotNull("Broadcast PendingIntent with FLAG_IMMUTABLE does not exist.", pi)
+ }
+
+ private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE)
+ private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT)
+
+ private fun verifyNotification(
+ iconId: Int,
+ title: String,
+ text: String,
+ id: Int,
+ intentSenderType: Int,
+ intent: Intent,
+ flags: Int
+ ) {
+ verify(notificationManager, never()).cancel(any(), eq(id))
+
+ val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
+ verify(notificationManager, times(1))
+ .notify(any(), eq(id), notificationCaptor.capture())
+
+ val notification = notificationCaptor.getValue()
+ assertEquals(iconId, notification.smallIcon.resId)
+ assertEquals(title, notification.title())
+ assertEquals(text, notification.text())
+
+ when (intentSenderType) {
+ ACTIVITY_PENDING_INTENT -> verifyActivityPendingIntent(intent, flags)
+ BROADCAST_PENDING_INTENT -> verifyBroadcastPendingIntent(intent, flags)
+ }
+
+ reset(notificationManager)
+ }
+
+ private fun verifyNotificationCancelled(
+ notificationIds: List<Int>,
+ resetAfterVerified: Boolean = true
+ ) {
+ notificationIds.forEach {
+ verify(notificationManager, times(1)).cancel(any(), eq(it))
+ }
+ if (resetAfterVerified) reset(notificationManager)
+ }
+
+ @Test
+ fun testRestrictedNotification() {
+ val settingsIntent = Intent(Settings.ACTION_TETHER_SETTINGS)
+ .setPackage(getSettingsPackageName(context.packageManager))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ // Set test sub id.
+ notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // User restrictions on. Show restricted notification.
+ notificationUpdater.notifyTetheringDisabledByRestriction()
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_DISALLOW_TITLE, TEST_DISALLOW_MESSAGE,
+ RESTRICTED_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE)
+
+ // User restrictions off. Clear notification.
+ notificationUpdater.tetheringRestrictionLifted()
+ verifyNotificationCancelled(listOf(RESTRICTED_NOTIFICATION_ID))
+
+ // No downstream.
+ notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+ verifyZeroInteractions(notificationManager)
+
+ // User restrictions on again. Show restricted notification.
+ notificationUpdater.notifyTetheringDisabledByRestriction()
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_DISALLOW_TITLE, TEST_DISALLOW_MESSAGE,
+ RESTRICTED_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE)
+ }
+
+ val MAX_BACKOFF_MS = 200L
+ /**
+ * Waits for all messages, including delayed ones, to be processed.
+ *
+ * This will wait until the handler has no more messages to be processed including
+ * delayed ones, or the timeout has expired. It uses an exponential backoff strategy
+ * to wait longer and longer to consume less CPU, with the max granularity being
+ * MAX_BACKOFF_MS.
+ *
+ * @return true if all messages have been processed including delayed ones, false if timeout
+ *
+ * TODO: Move this method to com.android.testutils.HandlerUtils.kt.
+ */
+ private fun Handler.waitForDelayedMessage(what: Int?, timeoutMs: Long) {
+ fun hasMatchingMessages() =
+ if (what == null) hasMessagesOrCallbacks() else hasMessages(what)
+ val expiry = System.currentTimeMillis() + timeoutMs
+ var delay = 5L
+ while (System.currentTimeMillis() < expiry && hasMatchingMessages()) {
+ // None of Handler, Looper, Message and MessageQueue expose any way to retrieve
+ // the time when the next (let alone the last) message will be processed, so
+ // short of examining the internals with reflection sleep() is the only solution.
+ Thread.sleep(delay)
+ delay = (delay * 2)
+ .coerceAtMost(expiry - System.currentTimeMillis())
+ .coerceAtMost(MAX_BACKOFF_MS)
+ }
+
+ val timeout = expiry - System.currentTimeMillis()
+ if (timeout <= 0) fail("Delayed message did not process yet after ${timeoutMs}ms")
+ waitForIdle(timeout)
+ }
+
+ @Test
+ fun testNoUpstreamNotification() {
+ val disableIntent = Intent(ACTION_DISABLE_TETHERING).setPackage(context.packageName)
+
+ // Set test sub id.
+ notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // Wifi downstream.
+ notificationUpdater.onDownstreamChanged(WIFI_MASK)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // There is no upstream. Show no upstream notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(null)
+ notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+ NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent,
+ FLAG_IMMUTABLE)
+
+ // Same capabilities changed. Nothing happened.
+ notificationUpdater.onUpstreamCapabilitiesChanged(null)
+ verifyZeroInteractions(notificationManager)
+
+ // Upstream come back. Clear no upstream notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID))
+
+ // No upstream again. Show no upstream notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(null)
+ notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+ NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent,
+ FLAG_IMMUTABLE)
+
+ // No downstream.
+ notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // Wifi downstream and home capabilities.
+ notificationUpdater.onDownstreamChanged(WIFI_MASK)
+ notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // Set R.integer.delay_to_show_no_upstream_after_no_backhaul to -1 and change to no upstream
+ // again. Don't put up no upstream notification.
+ doReturn(-1).`when`(testResources)
+ .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
+ notificationUpdater.onUpstreamCapabilitiesChanged(null)
+ notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID))
+ }
+
+ @Test
+ fun testGetResourcesForSubId() {
+ doReturn(telephonyManager).`when`(telephonyManager).createForSubscriptionId(anyInt())
+ doReturn(1234).`when`(telephonyManager).getSimCarrierId()
+ doReturn("000000").`when`(telephonyManager).getSimOperator()
+
+ val subId = -2 // Use invalid subId to avoid getting resource from cache or real subId.
+ val config = context.resources.configuration
+ var res = notificationUpdater.getResourcesForSubId(context, subId)
+ assertEquals(config.mcc, res.configuration.mcc)
+ assertEquals(config.mnc, res.configuration.mnc)
+
+ doReturn(VERIZON_CARRIER_ID).`when`(telephonyManager).getSimCarrierId()
+ res = notificationUpdater.getResourcesForSubId(context, subId)
+ assertEquals(config.mcc, res.configuration.mcc)
+ assertEquals(config.mnc, res.configuration.mnc)
+
+ doReturn("20404").`when`(telephonyManager).getSimOperator()
+ res = notificationUpdater.getResourcesForSubId(context, subId)
+ assertEquals(311, res.configuration.mcc)
+ assertEquals(480, res.configuration.mnc)
+ }
+
+ @Test
+ fun testRoamingNotification() {
+ val disableIntent = Intent(ACTION_DISABLE_TETHERING).setPackage(context.packageName)
+ val settingsIntent = Intent(Settings.ACTION_TETHER_SETTINGS)
+ .setPackage(getSettingsPackageName(context.packageManager))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ // Set test sub id.
+ notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // Wifi downstream.
+ notificationUpdater.onDownstreamChanged(WIFI_MASK)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // Upstream capabilities changed to roaming state. Show roaming notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_ROAMING_TITLE, TEST_ROAMING_MESSAGE,
+ ROAMING_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE)
+
+ // Same capabilities change. Nothing happened.
+ notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+ verifyZeroInteractions(notificationManager)
+
+ // Upstream capabilities changed to home state. Clear roaming notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
+ verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID))
+
+ // Upstream capabilities changed to roaming state again. Show roaming notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_ROAMING_TITLE, TEST_ROAMING_MESSAGE,
+ ROAMING_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE)
+
+ // No upstream. Clear roaming notification and show no upstream notification.
+ notificationUpdater.onUpstreamCapabilitiesChanged(null)
+ notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+ verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID), false)
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+ NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent,
+ FLAG_IMMUTABLE)
+
+ // No downstream.
+ notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+
+ // Wifi downstream again.
+ notificationUpdater.onDownstreamChanged(WIFI_MASK)
+ notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+ verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID), false)
+ verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+ NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent,
+ FLAG_IMMUTABLE)
+
+ // Set R.bool.config_upstream_roaming_notification to false and change upstream
+ // network to roaming state again. No roaming notification.
+ doReturn(false).`when`(testResources)
+ .getBoolean(R.bool.config_upstream_roaming_notification)
+ notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+ verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+ }
+
+ @Test
+ fun testGetSettingsPackageName() {
+ val defaultSettingsPackageName = "com.android.settings"
+ val testSettingsPackageName = "com.android.test.settings"
+ val pm = mock(PackageManager::class.java)
+ doReturn(null).`when`(pm).resolveActivity(any(), anyInt())
+ assertEquals(defaultSettingsPackageName, getSettingsPackageName(pm))
+
+ val resolveInfo = ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply {
+ name = "test"
+ applicationInfo = ApplicationInfo().apply {
+ packageName = testSettingsPackageName
+ }
+ }
+ }
+ doReturn(resolveInfo).`when`(pm).resolveActivity(any(), anyInt())
+ assertEquals(testSettingsPackageName, getSettingsPackageName(pm))
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
new file mode 100644
index 0000000..941cd78
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -0,0 +1,488 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.Manifest.permission.WRITE_SETTINGS;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.UiAutomation;
+import android.content.Intent;
+import android.net.IIntResultListener;
+import android.net.ITetheringConnector;
+import android.net.ITetheringEventCallback;
+import android.net.TetheringRequestParcel;
+import android.net.ip.IpServer;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.networkstack.tethering.MockTetheringService.MockTetheringConnector;
+
+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)
+@SmallTest
+public final class TetheringServiceTest {
+ private static final String TEST_IFACE_NAME = "test_wlan0";
+ private static final String TEST_CALLER_PKG = "com.android.shell";
+ private static final String TEST_ATTRIBUTION_TAG = null;
+ @Mock private ITetheringEventCallback mITetheringEventCallback;
+ @Rule public ServiceTestRule mServiceTestRule;
+ private Tethering mTethering;
+ private Intent mMockServiceIntent;
+ private ITetheringConnector mTetheringConnector;
+ private UiAutomation mUiAutomation;
+
+ private class TestTetheringResult extends IIntResultListener.Stub {
+ private int mResult = -1; // Default value that does not match any result code.
+ @Override
+ public void onResult(final int resultCode) {
+ mResult = resultCode;
+ }
+
+ public void assertResult(final int expected) {
+ assertEquals(expected, mResult);
+ }
+ }
+
+ private class MyResultReceiver extends ResultReceiver {
+ MyResultReceiver(Handler handler) {
+ super(handler);
+ }
+ private int mResult = -1; // Default value that does not match any result code.
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ mResult = resultCode;
+ }
+
+ public void assertResult(int expected) {
+ assertEquals(expected, mResult);
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ mServiceTestRule = new ServiceTestRule();
+ mMockServiceIntent = new Intent(
+ InstrumentationRegistry.getTargetContext(),
+ MockTetheringService.class);
+ final MockTetheringConnector mockConnector =
+ (MockTetheringConnector) mServiceTestRule.bindService(mMockServiceIntent);
+ mTetheringConnector = mockConnector.getTetheringConnector();
+ final MockTetheringService service = mockConnector.getService();
+ mTethering = service.getTethering();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mServiceTestRule.unbindService();
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ private interface TestTetheringCall {
+ void runTetheringCall(TestTetheringResult result) throws Exception;
+ }
+
+ private void runAsNoPermission(final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, new String[0]);
+ }
+
+ private void runAsTetherPrivileged(final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, TETHER_PRIVILEGED);
+ }
+
+ private void runAsAccessNetworkState(final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, ACCESS_NETWORK_STATE);
+ }
+
+ private void runAsWriteSettings(final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, WRITE_SETTINGS);
+ }
+
+ private void runTetheringCall(final TestTetheringCall test, String... permissions)
+ throws Exception {
+ if (permissions.length > 0) mUiAutomation.adoptShellPermissionIdentity(permissions);
+ try {
+ when(mTethering.isTetheringSupported()).thenReturn(true);
+ test.runTetheringCall(new TestTetheringResult());
+ } finally {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+ }
+
+ private void verifyNoMoreInteractionsForTethering() {
+ verifyNoMoreInteractions(mTethering);
+ verifyNoMoreInteractions(mITetheringEventCallback);
+ reset(mTethering, mITetheringEventCallback);
+ }
+
+ private void runTether(final TestTetheringResult result) throws Exception {
+ mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result);
+ }
+
+ @Test
+ public void testTether() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runTether(result);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runTether(result);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runUnTether(final TestTetheringResult result) throws Exception {
+ mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result));
+ }
+
+ @Test
+ public void testUntether() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runUnTether(result);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runUnTether(result);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runSetUsbTethering(final TestTetheringResult result) throws Exception {
+ doAnswer((invocation) -> {
+ final IIntResultListener listener = invocation.getArgument(1);
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ return null;
+ }).when(mTethering).setUsbTethering(anyBoolean(), any(IIntResultListener.class));
+ mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).setUsbTethering(eq(true) /* enable */, any(IIntResultListener.class));
+ result.assertResult(TETHER_ERROR_NO_ERROR);
+ }
+
+ @Test
+ public void testSetUsbTethering() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runSetUsbTethering(result);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runSetUsbTethering(result);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ }
+
+ private void runStartTethering(final TestTetheringResult result,
+ final TetheringRequestParcel request) throws Exception {
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).startTethering(eq(request), eq(result));
+ }
+
+ @Test
+ public void testStartTethering() throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_WIFI;
+
+ runAsNoPermission((result) -> {
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runStartTethering(result, request);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runStartTethering(result, request);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runStartTetheringAndVerifyNoPermission(final TestTetheringResult result)
+ throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_WIFI;
+ request.exemptFromEntitlementCheck = true;
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ }
+
+ @Test
+ public void testFailToBypassEntitlementWithoutNeworkStackPermission() throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_WIFI;
+ request.exemptFromEntitlementCheck = true;
+
+ runAsNoPermission((result) -> {
+ runStartTetheringAndVerifyNoPermission(result);
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runStartTetheringAndVerifyNoPermission(result);
+ });
+
+ runAsWriteSettings((result) -> {
+ runStartTetheringAndVerifyNoPermission(result);
+ });
+ }
+
+ private void runStopTethering(final TestTetheringResult result) throws Exception {
+ mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).stopTethering(TETHERING_WIFI);
+ result.assertResult(TETHER_ERROR_NO_ERROR);
+ }
+
+ @Test
+ public void testStopTethering() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runStopTethering(result);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runStopTethering(result);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runRequestLatestTetheringEntitlementResult() throws Exception {
+ final MyResultReceiver result = new MyResultReceiver(null);
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).requestLatestTetheringEntitlementResult(eq(TETHERING_WIFI),
+ eq(result), eq(true) /* showEntitlementUi */);
+ }
+
+ @Test
+ public void testRequestLatestTetheringEntitlementResult() throws Exception {
+ // Run as no permission.
+ final MyResultReceiver result = new MyResultReceiver(null);
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractions(mTethering);
+
+ runAsTetherPrivileged((none) -> {
+ runRequestLatestTetheringEntitlementResult();
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((none) -> {
+ runRequestLatestTetheringEntitlementResult();
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runRegisterTetheringEventCallback() throws Exception {
+ mTetheringConnector.registerTetheringEventCallback(mITetheringEventCallback,
+ TEST_CALLER_PKG);
+ verify(mTethering).registerTetheringEventCallback(eq(mITetheringEventCallback));
+ }
+
+ @Test
+ public void testRegisterTetheringEventCallback() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.registerTetheringEventCallback(mITetheringEventCallback,
+ TEST_CALLER_PKG);
+ verify(mITetheringEventCallback).onCallbackStopped(
+ TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((none) -> {
+ runRegisterTetheringEventCallback();
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsAccessNetworkState((none) -> {
+ runRegisterTetheringEventCallback();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runUnregisterTetheringEventCallback() throws Exception {
+ mTetheringConnector.unregisterTetheringEventCallback(mITetheringEventCallback,
+ TEST_CALLER_PKG);
+ verify(mTethering).unregisterTetheringEventCallback(eq(mITetheringEventCallback));
+ }
+
+ @Test
+ public void testUnregisterTetheringEventCallback() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.unregisterTetheringEventCallback(mITetheringEventCallback,
+ TEST_CALLER_PKG);
+ verify(mITetheringEventCallback).onCallbackStopped(
+ TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((none) -> {
+ runUnregisterTetheringEventCallback();
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsAccessNetworkState((none) -> {
+ runUnregisterTetheringEventCallback();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runStopAllTethering(final TestTetheringResult result) throws Exception {
+ mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringSupported();
+ verify(mTethering).untetherAll();
+ result.assertResult(TETHER_ERROR_NO_ERROR);
+ }
+
+ @Test
+ public void testStopAllTethering() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runStopAllTethering(result);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runStopAllTethering(result);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ private void runIsTetheringSupported(final TestTetheringResult result) throws Exception {
+ mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringSupported();
+ result.assertResult(TETHER_ERROR_NO_ERROR);
+ }
+
+ @Test
+ public void testIsTetheringSupported() throws Exception {
+ runAsNoPermission((result) -> {
+ mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetherProvisioningRequired();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
+ runIsTetheringSupported(result);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettings((result) -> {
+ runIsTetheringSupported(result);
+ verify(mTethering).isTetherProvisioningRequired();
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
new file mode 100644
index 0000000..2b15866
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -0,0 +1,2572 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.GET_ACTIVITIES;
+import static android.hardware.usb.UsbManager.USB_CONFIGURED;
+import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
+import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
+import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
+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;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
+import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
+import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
+import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
+import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
+import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertArrayEquals;
+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.argThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.NetworkStatsManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.EthernetManager;
+import android.net.EthernetManager.TetheredInterfaceCallback;
+import android.net.EthernetManager.TetheredInterfaceRequest;
+import android.net.IConnectivityManager;
+import android.net.IIntResultListener;
+import android.net.INetd;
+import android.net.ITetheringEventCallback;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+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.TetherStatesParcel;
+import android.net.TetheredClient;
+import android.net.TetheredClient.AddressInfo;
+import android.net.TetheringCallbackStartedParcel;
+import android.net.TetheringConfigurationParcel;
+import android.net.TetheringInterface;
+import android.net.TetheringRequestParcel;
+import android.net.dhcp.DhcpLeaseParcelable;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpEventCallbacks;
+import android.net.dhcp.IDhcpServer;
+import android.net.ip.DadProxy;
+import android.net.ip.IpNeighborMonitor;
+import android.net.ip.IpServer;
+import android.net.ip.RouterAdvertisementDaemon;
+import android.net.util.InterfaceParams;
+import android.net.util.NetworkConstants;
+import android.net.util.SharedLog;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiClient;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.SoftApCallback;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
+import com.android.testutils.MiscAsserts;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+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;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Vector;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TetheringTest {
+ private static final int IFINDEX_OFFSET = 100;
+
+ private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
+ private static final String TEST_DUN_IFNAME = "test_dun0";
+ private static final String TEST_XLAT_MOBILE_IFNAME = "v4-test_rmnet_data0";
+ private static final String TEST_USB_IFNAME = "test_rndis0";
+ private static final String TEST_WIFI_IFNAME = "test_wlan0";
+ private static final String TEST_WLAN_IFNAME = "test_wlan1";
+ private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
+ private static final String TEST_NCM_IFNAME = "test_ncm0";
+ private static final String TEST_ETH_IFNAME = "test_eth0";
+ private static final String TEST_BT_IFNAME = "test_pan0";
+ private static final String TETHERING_NAME = "Tethering";
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+ private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
+
+ private static final int CELLULAR_NETID = 100;
+ private static final int WIFI_NETID = 101;
+ private static final int DUN_NETID = 102;
+
+ private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
+
+ @Mock private ApplicationInfo mApplicationInfo;
+ @Mock private Context mContext;
+ @Mock private NetworkStatsManager mStatsManager;
+ @Mock private OffloadHardwareInterface mOffloadHardwareInterface;
+ @Mock private OffloadHardwareInterface.ForwardedStats mForwardedStats;
+ @Mock private Resources mResources;
+ @Mock private TelephonyManager mTelephonyManager;
+ @Mock private UsbManager mUsbManager;
+ @Mock private WifiManager mWifiManager;
+ @Mock private CarrierConfigManager mCarrierConfigManager;
+ @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+ @Mock private DadProxy mDadProxy;
+ @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
+ @Mock private IpNeighborMonitor mIpNeighborMonitor;
+ @Mock private IDhcpServer mDhcpServer;
+ @Mock private INetd mNetd;
+ @Mock private UserManager mUserManager;
+ @Mock private EthernetManager mEm;
+ @Mock private TetheringNotificationUpdater mNotificationUpdater;
+ @Mock private BpfCoordinator mBpfCoordinator;
+ @Mock private PackageManager mPackageManager;
+ @Mock private BluetoothAdapter mBluetoothAdapter;
+ @Mock private BluetoothPan mBluetoothPan;
+
+ private final MockIpServerDependencies mIpServerDependencies =
+ spy(new MockIpServerDependencies());
+ private final MockTetheringDependencies mTetheringDependencies =
+ new MockTetheringDependencies();
+
+ // Like so many Android system APIs, these cannot be mocked because it is marked final.
+ // We have to use the real versions.
+ private final PersistableBundle mCarrierConfig = new PersistableBundle();
+ private final TestLooper mLooper = new TestLooper();
+
+ private Vector<Intent> mIntents;
+ private BroadcastInterceptingContext mServiceContext;
+ private MockContentResolver mContentResolver;
+ private BroadcastReceiver mBroadcastReceiver;
+ private Tethering mTethering;
+ private PhoneStateListener mPhoneStateListener;
+ private InterfaceConfigurationParcel mInterfaceConfiguration;
+ private TetheringConfiguration mConfig;
+ private EntitlementManager mEntitleMgr;
+ private OffloadController mOffloadCtrl;
+ private PrivateAddressCoordinator mPrivateAddressCoordinator;
+ private SoftApCallback mSoftApCallback;
+ private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+
+ private TestConnectivityManager mCm;
+
+ private class TestContext extends BroadcastInterceptingContext {
+ TestContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ return mApplicationInfo;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @Override
+ public String getPackageName() {
+ return "TetheringTest";
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.WIFI_SERVICE.equals(name)) return mWifiManager;
+ if (Context.USB_SERVICE.equals(name)) return mUsbManager;
+ if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
+ if (Context.USER_SERVICE.equals(name)) return mUserManager;
+ if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager;
+ if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm;
+ if (Context.ETHERNET_SERVICE.equals(name)) return mEm;
+ return super.getSystemService(name);
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+ return super.getSystemServiceName(serviceClass);
+ }
+ }
+
+ public class MockIpServerDependencies extends IpServer.Dependencies {
+ @Override
+ public DadProxy getDadProxy(
+ Handler handler, InterfaceParams ifParams) {
+ return mDadProxy;
+ }
+
+ @Override
+ public RouterAdvertisementDaemon getRouterAdvertisementDaemon(
+ InterfaceParams ifParams) {
+ return mRouterAdvertisementDaemon;
+ }
+
+ @Override
+ public InterfaceParams getInterfaceParams(String ifName) {
+ assertTrue("Non-mocked interface " + ifName,
+ ifName.equals(TEST_USB_IFNAME)
+ || ifName.equals(TEST_WLAN_IFNAME)
+ || ifName.equals(TEST_WIFI_IFNAME)
+ || ifName.equals(TEST_MOBILE_IFNAME)
+ || ifName.equals(TEST_DUN_IFNAME)
+ || ifName.equals(TEST_P2P_IFNAME)
+ || ifName.equals(TEST_NCM_IFNAME)
+ || ifName.equals(TEST_ETH_IFNAME)
+ || ifName.equals(TEST_BT_IFNAME));
+ final String[] ifaces = new String[] {
+ TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME,
+ TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME};
+ return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
+ MacAddress.ALL_ZEROS_ADDRESS);
+ }
+
+ @Override
+ public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+ DhcpServerCallbacks cb) {
+ new Thread(() -> {
+ try {
+ cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }).run();
+ }
+
+ public IpNeighborMonitor getIpNeighborMonitor(Handler h, SharedLog l,
+ IpNeighborMonitor.NeighborEventConsumer c) {
+ return mIpNeighborMonitor;
+ }
+ }
+
+ // MyTetheringConfiguration is used to override static method for testing.
+ private class MyTetheringConfiguration extends TetheringConfiguration {
+ MyTetheringConfiguration(Context ctx, SharedLog log, int id) {
+ super(ctx, log, id);
+ }
+
+ @Override
+ protected String getDeviceConfigProperty(final String name) {
+ return null;
+ }
+
+ @Override
+ protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+ return false;
+ }
+
+ @Override
+ protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
+ return mResources;
+ }
+ }
+
+ public class MockTetheringDependencies extends TetheringDependencies {
+ StateMachine mUpstreamNetworkMonitorSM;
+ ArrayList<IpServer> mIpv6CoordinatorNotifyList;
+
+ public void reset() {
+ mUpstreamNetworkMonitorSM = null;
+ mIpv6CoordinatorNotifyList = null;
+ }
+
+ @Override
+ public BpfCoordinator getBpfCoordinator(
+ BpfCoordinator.Dependencies deps) {
+ return mBpfCoordinator;
+ }
+
+ @Override
+ public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+ return mOffloadHardwareInterface;
+ }
+
+ @Override
+ public OffloadController getOffloadController(Handler h, SharedLog log,
+ OffloadController.Dependencies deps) {
+ mOffloadCtrl = spy(super.getOffloadController(h, log, deps));
+ // Return real object here instead of mock because
+ // testReportFailCallbackIfOffloadNotSupported depend on real OffloadController object.
+ return mOffloadCtrl;
+ }
+
+ @Override
+ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
+ StateMachine target, SharedLog log, int what) {
+ // Use a real object instead of a mock so that some tests can use a real UNM and some
+ // can use a mock.
+ mUpstreamNetworkMonitorSM = target;
+ mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what));
+ return mUpstreamNetworkMonitor;
+ }
+
+ @Override
+ public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+ ArrayList<IpServer> notifyList, SharedLog log) {
+ mIpv6CoordinatorNotifyList = notifyList;
+ return mIPv6TetheringCoordinator;
+ }
+
+ @Override
+ public IpServer.Dependencies getIpServerDependencies() {
+ return mIpServerDependencies;
+ }
+
+ @Override
+ public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
+ Runnable callback) {
+ mEntitleMgr = spy(super.getEntitlementManager(ctx, h, log, callback));
+ return mEntitleMgr;
+ }
+
+ @Override
+ public boolean isTetheringSupported() {
+ return true;
+ }
+
+ @Override
+ public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
+ int subId) {
+ mConfig = spy(new MyTetheringConfiguration(ctx, log, subId));
+ return mConfig;
+ }
+
+ @Override
+ public INetd getINetd(Context context) {
+ return mNetd;
+ }
+
+ @Override
+ public Looper getTetheringLooper() {
+ return mLooper.getLooper();
+ }
+
+ @Override
+ public Context getContext() {
+ return mServiceContext;
+ }
+
+ @Override
+ public BluetoothAdapter getBluetoothAdapter() {
+ return mBluetoothAdapter;
+ }
+
+ @Override
+ public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) {
+ return mNotificationUpdater;
+ }
+
+ @Override
+ public boolean isTetheringDenied() {
+ return false;
+ }
+
+
+ @Override
+ public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
+ TetheringConfiguration cfg) {
+ mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
+ return mPrivateAddressCoordinator;
+ }
+ }
+
+ private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
+ boolean withIPv4, boolean withIPv6, boolean with464xlat) {
+ final LinkProperties prop = new LinkProperties();
+ prop.setInterfaceName(interfaceName);
+
+ if (withIPv4) {
+ prop.addLinkAddress(new LinkAddress("10.1.2.3/15"));
+ prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+ InetAddresses.parseNumericAddress("10.0.0.1"),
+ interfaceName, RTN_UNICAST));
+ }
+
+ if (withIPv6) {
+ prop.addDnsServer(InetAddresses.parseNumericAddress("2001:db8::2"));
+ prop.addLinkAddress(
+ new LinkAddress(InetAddresses.parseNumericAddress("2001:db8::"),
+ NetworkConstants.RFC7421_PREFIX_LENGTH));
+ prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ interfaceName, RTN_UNICAST));
+ }
+
+ if (with464xlat) {
+ final String clatInterface = "v4-" + interfaceName;
+ final LinkProperties stackedLink = new LinkProperties();
+ stackedLink.setInterfaceName(clatInterface);
+ stackedLink.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+ InetAddresses.parseNumericAddress("192.0.0.1"),
+ clatInterface, RTN_UNICAST));
+
+ prop.addStackedLink(stackedLink);
+ }
+
+ return prop;
+ }
+
+ private static NetworkCapabilities buildUpstreamCapabilities(int transport, int... otherCaps) {
+ // TODO: add NOT_VCN_MANAGED.
+ final NetworkCapabilities nc = new NetworkCapabilities()
+ .addTransportType(transport);
+ for (int cap : otherCaps) {
+ nc.addCapability(cap);
+ }
+ return nc;
+ }
+
+ private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4,
+ boolean withIPv6, boolean with464xlat) {
+ return new UpstreamNetworkState(
+ buildUpstreamLinkProperties(TEST_MOBILE_IFNAME, withIPv4, withIPv6, with464xlat),
+ buildUpstreamCapabilities(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET),
+ new Network(CELLULAR_NETID));
+ }
+
+ private static UpstreamNetworkState buildMobileIPv4UpstreamState() {
+ return buildMobileUpstreamState(true, false, false);
+ }
+
+ private static UpstreamNetworkState buildMobileIPv6UpstreamState() {
+ return buildMobileUpstreamState(false, true, false);
+ }
+
+ private static UpstreamNetworkState buildMobileDualStackUpstreamState() {
+ return buildMobileUpstreamState(true, true, false);
+ }
+
+ private static UpstreamNetworkState buildMobile464xlatUpstreamState() {
+ return buildMobileUpstreamState(false, true, true);
+ }
+
+ private static UpstreamNetworkState buildWifiUpstreamState() {
+ return new UpstreamNetworkState(
+ buildUpstreamLinkProperties(TEST_WIFI_IFNAME, true /* IPv4 */, true /* IPv6 */,
+ false /* 464xlat */),
+ buildUpstreamCapabilities(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET),
+ new Network(WIFI_NETID));
+ }
+
+ private static UpstreamNetworkState buildDunUpstreamState() {
+ return new UpstreamNetworkState(
+ buildUpstreamLinkProperties(TEST_DUN_IFNAME, true /* IPv4 */, true /* IPv6 */,
+ false /* 464xlat */),
+ buildUpstreamCapabilities(TRANSPORT_CELLULAR, NET_CAPABILITY_DUN),
+ new Network(DUN_NETID));
+ }
+
+ // See FakeSettingsProvider#clearSettingsProvider() that this needs to be called before and
+ // after use.
+ @BeforeClass
+ public static void setupOnce() {
+ FakeSettingsProvider.clearSettingsProvider();
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ FakeSettingsProvider.clearSettingsProvider();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mResources.getStringArray(R.array.config_tether_dhcp_range))
+ .thenReturn(new String[0]);
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ false);
+ when(mNetd.interfaceGetList())
+ .thenReturn(new String[] {
+ TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME,
+ TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
+ when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
+ mInterfaceConfiguration = new InterfaceConfigurationParcel();
+ mInterfaceConfiguration.flags = new String[0];
+ when(mRouterAdvertisementDaemon.start())
+ .thenReturn(true);
+ initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
+ 0 /* defaultDisabled */);
+ when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats);
+
+ mServiceContext = new TestContext(mContext);
+ mServiceContext.setUseRegisteredHandlers(true);
+ mContentResolver = new MockContentResolver(mServiceContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ setTetheringSupported(true /* supported */);
+ mIntents = new Vector<>();
+ mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntents.addElement(intent);
+ }
+ };
+ mServiceContext.registerReceiver(mBroadcastReceiver,
+ new IntentFilter(ACTION_TETHER_STATE_CHANGED));
+
+ mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
+
+ mTethering = makeTethering();
+ verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
+ verify(mNetd).registerUnsolicitedEventListener(any());
+ verifyDefaultNetworkRequestFiled();
+
+ final ArgumentCaptor<PhoneStateListener> phoneListenerCaptor =
+ ArgumentCaptor.forClass(PhoneStateListener.class);
+ verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
+ eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE));
+ mPhoneStateListener = phoneListenerCaptor.getValue();
+
+ final ArgumentCaptor<SoftApCallback> softApCallbackCaptor =
+ ArgumentCaptor.forClass(SoftApCallback.class);
+ verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
+ mSoftApCallback = softApCallbackCaptor.getValue();
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
+ }
+
+ private void setTetheringSupported(final boolean supported) {
+ Settings.Global.putInt(mContentResolver, Settings.Global.TETHER_SUPPORTED,
+ supported ? 1 : 0);
+ when(mUserManager.hasUserRestriction(
+ UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(!supported);
+ // Setup tetherable configuration.
+ when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+ .thenReturn(new String[] { "test_rndis\\d" });
+ when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
+ .thenReturn(new String[] { "test_wlan\\d" });
+ when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
+ .thenReturn(new String[] { "test_p2p-p2p\\d-.*" });
+ when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+ .thenReturn(new String[] { "test_pan\\d" });
+ when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
+ .thenReturn(new String[] { "test_ncm\\d" });
+ when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
+ new int[] { TYPE_WIFI, TYPE_MOBILE_DUN });
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
+ }
+
+ private void initTetheringUpstream(UpstreamNetworkState upstreamState) {
+ doReturn(upstreamState).when(mUpstreamNetworkMonitor).getCurrentPreferredUpstream();
+ doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
+ }
+
+ private Tethering makeTethering() {
+ mTetheringDependencies.reset();
+ return new Tethering(mTetheringDependencies);
+ }
+
+ private TetheringRequestParcel createTetheringRequestParcel(final int type) {
+ return createTetheringRequestParcel(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;
+ }
+
+ @After
+ public void tearDown() {
+ mServiceContext.unregisterReceiver(mBroadcastReceiver);
+ }
+
+ private void sendWifiApStateChanged(int state) {
+ final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ intent.putExtra(EXTRA_WIFI_AP_STATE, state);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
+ }
+
+ private void sendWifiApStateChanged(int state, String ifname, int ipmode) {
+ final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ intent.putExtra(EXTRA_WIFI_AP_STATE, state);
+ intent.putExtra(EXTRA_WIFI_AP_INTERFACE_NAME, ifname);
+ intent.putExtra(EXTRA_WIFI_AP_MODE, ipmode);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
+ }
+
+ private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = {
+ android.Manifest.permission.ACCESS_FINE_LOCATION,
+ android.Manifest.permission.ACCESS_WIFI_STATE
+ };
+
+ private void sendWifiP2pConnectionChanged(
+ boolean isGroupFormed, boolean isGroupOwner, String ifname) {
+ WifiP2pGroup group = null;
+ WifiP2pInfo p2pInfo = new WifiP2pInfo();
+ p2pInfo.groupFormed = isGroupFormed;
+ if (isGroupFormed) {
+ p2pInfo.isGroupOwner = isGroupOwner;
+ group = mock(WifiP2pGroup.class);
+ when(group.isGroupOwner()).thenReturn(isGroupOwner);
+ when(group.getInterface()).thenReturn(ifname);
+ }
+
+ final Intent intent = mock(Intent.class);
+ when(intent.getAction()).thenReturn(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+ when(intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO)).thenReturn(p2pInfo);
+ when(intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)).thenReturn(group);
+
+ mServiceContext.sendBroadcastAsUserMultiplePermissions(intent, UserHandle.ALL,
+ P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST);
+ mLooper.dispatchAll();
+ }
+
+ private void sendUsbBroadcast(boolean connected, boolean configured, boolean function,
+ int type) {
+ final Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
+ intent.putExtra(USB_CONNECTED, connected);
+ intent.putExtra(USB_CONFIGURED, configured);
+ if (type == TETHERING_USB) {
+ intent.putExtra(USB_FUNCTION_RNDIS, function);
+ } else {
+ intent.putExtra(USB_FUNCTION_NCM, function);
+ }
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
+ }
+
+ private void sendConfigurationChanged() {
+ final Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
+ }
+
+ private void verifyDefaultNetworkRequestFiled() {
+ ArgumentCaptor<NetworkRequest> reqCaptor = ArgumentCaptor.forClass(NetworkRequest.class);
+ verify(mCm, times(1)).requestNetwork(reqCaptor.capture(),
+ any(NetworkCallback.class), any(Handler.class));
+ assertTrue(TestConnectivityManager.looksLikeDefaultRequest(reqCaptor.getValue()));
+ // The default network request is only ever filed once.
+ verifyNoMoreInteractions(mCm);
+ mUpstreamNetworkMonitor.startTrackDefaultNetwork(mEntitleMgr);
+ verifyNoMoreInteractions(mCm);
+ }
+
+ private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
+ verify(mNetd, times(1)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, times(1)).tetherInterfaceAdd(ifname);
+ verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, ifname);
+ verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname),
+ anyString(), anyString());
+ }
+
+ private void verifyTetheringBroadcast(String ifname, String whichExtra) {
+ // Verify that ifname is in the whichExtra array of the tether state changed broadcast.
+ final Intent bcast = mIntents.get(0);
+ assertEquals(ACTION_TETHER_STATE_CHANGED, bcast.getAction());
+ final ArrayList<String> ifnames = bcast.getStringArrayListExtra(whichExtra);
+ assertTrue(ifnames.contains(ifname));
+ mIntents.remove(bcast);
+ }
+
+ public void failingLocalOnlyHotspotLegacyApBroadcast(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // hotspot mode is to be started.
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+
+ // If, and only if, Tethering received an interface status changed then
+ // it creates a IpServer and sends out a broadcast indicating that the
+ // interface is "available".
+ if (emulateInterfaceStatusChanged) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
+ verifyNoMoreInteractions(mNetd);
+ verifyNoMoreInteractions(mWifiManager);
+ }
+
+ private void prepareNcmTethering() {
+ // Emulate startTethering(TETHERING_NCM) called
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), null);
+ mLooper.dispatchAll();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
+
+ mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
+ }
+
+ private void prepareUsbTethering() {
+ // Emulate pressing the USB tethering button in Settings UI.
+ final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
+ mTethering.startTethering(request, null);
+ mLooper.dispatchAll();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ assertEquals(1, mTethering.getActiveTetheringRequests().size());
+ assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB));
+
+ mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+ }
+
+ @Test
+ public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+ UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ initTetheringUpstream(upstreamState);
+ prepareUsbTethering();
+
+ // This should produce no activity of any kind.
+ verifyNoMoreInteractions(mNetd);
+
+ // Pretend we then receive USB configured broadcast.
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ // Now we should see the start of tethering mechanics (in this case:
+ // tetherMatchingInterfaces() which starts by fetching all interfaces).
+ verify(mNetd, times(1)).interfaceGetList();
+
+ // UpstreamNetworkMonitor should receive selected upstream
+ verify(mUpstreamNetworkMonitor, times(1)).getCurrentPreferredUpstream();
+ verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
+ }
+
+ @Test
+ public void failingLocalOnlyHotspotLegacyApBroadcastWithIfaceStatusChanged() throws Exception {
+ failingLocalOnlyHotspotLegacyApBroadcast(true);
+ }
+
+ @Test
+ public void failingLocalOnlyHotspotLegacyApBroadcastSansIfaceStatusChanged() throws Exception {
+ failingLocalOnlyHotspotLegacyApBroadcast(false);
+ }
+
+ public void workingLocalOnlyHotspotEnrichedApBroadcast(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // hotspot mode is to be started.
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+ verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
+ verify(mNetd, times(1)).tetherStartWithConfiguration(any());
+ verifyNoMoreInteractions(mNetd);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_LOCAL_ONLY);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
+ verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
+
+ // Emulate externally-visible WifiManager effects, when hotspot mode
+ // is being torn down.
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME);
+ verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+ // interfaceSetCfg() called once for enabling and twice disabling IPv4.
+ verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, times(1)).tetherStop();
+ verify(mNetd, times(1)).ipfwdDisableForwarding(TETHERING_NAME);
+ verify(mWifiManager, times(3)).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verifyNoMoreInteractions(mNetd);
+ verifyNoMoreInteractions(mWifiManager);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_WLAN_IFNAME));
+ }
+
+ /**
+ * Send CMD_IPV6_TETHER_UPDATE to IpServers as would be done by IPv6TetheringCoordinator.
+ */
+ private void sendIPv6TetherUpdates(UpstreamNetworkState upstreamState) {
+ // IPv6TetheringCoordinator must have been notified of downstream
+ verify(mIPv6TetheringCoordinator, times(1)).addActiveDownstream(
+ argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_USB_IFNAME)),
+ eq(IpServer.STATE_TETHERED));
+
+ for (IpServer ipSrv : mTetheringDependencies.mIpv6CoordinatorNotifyList) {
+ UpstreamNetworkState ipv6OnlyState = buildMobileUpstreamState(false, true, false);
+ ipSrv.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0,
+ upstreamState.linkProperties.isIpv6Provisioned()
+ ? ipv6OnlyState.linkProperties
+ : null);
+ }
+ mLooper.dispatchAll();
+ }
+
+ private void runUsbTethering(UpstreamNetworkState upstreamState) {
+ initTetheringUpstream(upstreamState);
+ prepareUsbTethering();
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ }
+
+ private void assertSetIfaceToDadProxy(final int numOfCalls, final String ifaceName) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R || "S".equals(Build.VERSION.CODENAME)
+ || "T".equals(Build.VERSION.CODENAME)) {
+ verify(mDadProxy, times(numOfCalls)).setUpstreamIface(
+ argThat(ifaceParams -> ifaceName.equals(ifaceParams.name)));
+ }
+ }
+
+ @Test
+ public void workingMobileUsbTethering_IPv4() throws Exception {
+ UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ sendIPv6TetherUpdates(upstreamState);
+ assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */);
+ verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ }
+
+ @Test
+ public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ true);
+ sendConfigurationChanged();
+ final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ runUsbTethering(upstreamState);
+ sendIPv6TetherUpdates(upstreamState);
+
+ verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any());
+ }
+
+ @Test
+ public void workingMobileUsbTethering_IPv6() throws Exception {
+ UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ sendIPv6TetherUpdates(upstreamState);
+ // TODO: add interfaceParams to compare in verify.
+ assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
+ verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_DualStack() throws Exception {
+ UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRouterAdvertisementDaemon, times(1)).start();
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+
+ sendIPv6TetherUpdates(upstreamState);
+ assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
+ verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+ UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ sendIPv6TetherUpdates(upstreamState);
+ assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
+ verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+ // Setup IPv6
+ UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ // Then 464xlat comes up
+ upstreamState = buildMobile464xlatUpstreamState();
+ initTetheringUpstream(upstreamState);
+
+ // Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES.
+ mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
+ Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
+ UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
+ 0,
+ upstreamState);
+ mLooper.dispatchAll();
+
+ // Forwarding is added for 464xlat
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ // Forwarding was not re-added for v6 (still times(1))
+ verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ // DHCP not restarted on downstream (still times(1))
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ }
+
+ @Test
+ public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
+ sendConfigurationChanged();
+
+ // Setup IPv6
+ final UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
+ runUsbTethering(upstreamState);
+
+ // UpstreamNetworkMonitor should choose upstream automatically
+ // (in this specific case: choose the default network).
+ verify(mUpstreamNetworkMonitor, times(1)).getCurrentPreferredUpstream();
+ verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any());
+
+ verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
+ }
+
+ private void upstreamSelectionTestCommon(final boolean automatic, InOrder inOrder,
+ TestNetworkAgent mobile, TestNetworkAgent wifi) throws Exception {
+ // Enable automatic upstream selection.
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(automatic);
+ sendConfigurationChanged();
+ mLooper.dispatchAll();
+
+ // Start USB tethering with no current upstream.
+ prepareUsbTethering();
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
+
+ // Pretend cellular connected and expect the upstream to be set.
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ // Switch upstream to wifi.
+ wifi.fakeConnect();
+ mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+ }
+
+ @Test
+ public void testAutomaticUpstreamSelection() throws Exception {
+ TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
+ InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
+ // Enable automatic upstream selection.
+ upstreamSelectionTestCommon(true, inOrder, mobile, wifi);
+
+ // This code has historically been racy, so test different orderings of CONNECTIVITY_ACTION
+ // broadcasts and callbacks, and add mLooper.dispatchAll() calls between the two.
+ final Runnable doDispatchAll = () -> mLooper.dispatchAll();
+
+ // Switch upstreams a few times.
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ // Wifi disconnecting should not have any affect since it's not the current upstream.
+ wifi.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any());
+
+ // Lose and regain upstream.
+ assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+ .hasIPv4Address());
+ mCm.makeDefaultNetwork(null, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ mobile.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+
+ mobile = new TestNetworkAgent(mCm, buildMobile464xlatUpstreamState());
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ // Check the IP addresses to ensure that the upstream is indeed not the same as the previous
+ // mobile upstream, even though the netId is (unrealistically) the same.
+ assertFalse(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+ .hasIPv4Address());
+
+ // Lose and regain upstream again.
+ mCm.makeDefaultNetwork(null, CALLBACKS_FIRST, doDispatchAll);
+ mobile.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+
+ mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+ .hasIPv4Address());
+
+ // Check that the code does not crash if onLinkPropertiesChanged is received after onLost.
+ mobile.fakeDisconnect();
+ mobile.sendLinkProperties();
+ mLooper.dispatchAll();
+ }
+
+ @Test
+ public void testLegacyUpstreamSelection() throws Exception {
+ TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
+ InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
+ // Enable legacy upstream selection.
+ upstreamSelectionTestCommon(false, inOrder, mobile, wifi);
+
+ // Wifi disconnecting and the default network switch to mobile, the upstream should also
+ // switch to mobile.
+ wifi.fakeDisconnect();
+ mLooper.dispatchAll();
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, null);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ wifi.fakeConnect();
+ mLooper.dispatchAll();
+ mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST, null);
+ mLooper.dispatchAll();
+ }
+
+ @Test
+ public void testChooseDunUpstreamByAutomaticMode() throws Exception {
+ // Enable automatic upstream selection.
+ TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
+ TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
+ InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
+ chooseDunUpstreamTestCommon(true, inOrder, mobile, wifi, dun);
+
+ // When default network switch to mobile and wifi is connected (may have low signal),
+ // automatic mode would request dun again and choose it as upstream.
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
+ inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(), any());
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+ final Runnable doDispatchAll = () -> mLooper.dispatchAll();
+ dun.fakeConnect(CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId);
+
+ // Lose and regain upstream again.
+ dun.fakeDisconnect(CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+ inOrder.verify(mCm, never()).unregisterNetworkCallback(any(NetworkCallback.class));
+ dun.fakeConnect(CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId);
+ }
+
+ @Test
+ public void testChooseDunUpstreamByLegacyMode() throws Exception {
+ // Enable Legacy upstream selection.
+ TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
+ TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
+ InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
+ chooseDunUpstreamTestCommon(false, inOrder, mobile, wifi, dun);
+
+ // Legacy mode would keep use wifi as upstream (because it has higher priority in the
+ // list).
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any());
+ // BUG: when wifi disconnect, the dun request would not be filed again because wifi is
+ // no longer be default network which do not have CONNECTIVIY_ACTION broadcast.
+ wifi.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mCm, never()).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
+ any());
+
+ // Change the legacy priority list that dun is higher than wifi.
+ when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
+ new int[] { TYPE_MOBILE_DUN, TYPE_WIFI });
+ sendConfigurationChanged();
+ mLooper.dispatchAll();
+
+ // Make wifi as default network. Note: mobile also connected.
+ wifi.fakeConnect();
+ mLooper.dispatchAll();
+ mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ // BUG: dun has higher priority than wifi but tethering don't file dun request because
+ // current upstream is wifi.
+ inOrder.verify(mCm, never()).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
+ any());
+ }
+
+ private void chooseDunUpstreamTestCommon(final boolean automatic, InOrder inOrder,
+ TestNetworkAgent mobile, TestNetworkAgent wifi, TestNetworkAgent dun) throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(automatic);
+ when(mTelephonyManager.isTetheringApnRequired()).thenReturn(true);
+ sendConfigurationChanged();
+ mLooper.dispatchAll();
+
+ // Start USB tethering with no current upstream.
+ prepareUsbTethering();
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
+ ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
+ inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
+ captor.capture());
+ final NetworkCallback dunNetworkCallback1 = captor.getValue();
+
+ // Pretend cellular connected and expect the upstream to be set.
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(mobile.networkId);
+
+ // Pretend dun connected and expect choose dun as upstream.
+ final Runnable doDispatchAll = () -> mLooper.dispatchAll();
+ dun.fakeConnect(BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId);
+
+ // When wifi connected, unregister dun request and choose wifi as upstream.
+ wifi.fakeConnect();
+ mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
+ inOrder.verify(mCm).unregisterNetworkCallback(eq(dunNetworkCallback1));
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+ dun.fakeDisconnect(BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any());
+ }
+
+ private void runNcmTethering() {
+ prepareNcmTethering();
+ sendUsbBroadcast(true, true, true, TETHERING_NCM);
+ }
+
+ @Test
+ public void workingNcmTethering() throws Exception {
+ runNcmTethering();
+
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ }
+
+ @Test
+ public void workingNcmTethering_LegacyDhcp() {
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ true);
+ sendConfigurationChanged();
+ runNcmTethering();
+
+ verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any());
+ }
+
+ @Test
+ public void workingLocalOnlyHotspotEnrichedApBroadcastWithIfaceChanged() throws Exception {
+ workingLocalOnlyHotspotEnrichedApBroadcast(true);
+ }
+
+ @Test
+ public void workingLocalOnlyHotspotEnrichedApBroadcastSansIfaceChanged() throws Exception {
+ workingLocalOnlyHotspotEnrichedApBroadcast(false);
+ }
+
+ // TODO: Test with and without interfaceStatusChanged().
+ @Test
+ public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+ when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startTetheredHotspot(null);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNetd);
+
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // tethering mode is to be started.
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+
+ // There is 1 IpServer state change event: STATE_AVAILABLE
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verifyNoMoreInteractions(mNetd);
+ verifyNoMoreInteractions(mWifiManager);
+ }
+
+ // TODO: Test with and without interfaceStatusChanged().
+ @Test
+ public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+ when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startTetheredHotspot(null);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNetd);
+
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // tethering mode is to be started.
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
+ verify(mNetd, times(1)).tetherStartWithConfiguration(any());
+ verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME),
+ anyString(), anyString());
+ verifyNoMoreInteractions(mNetd);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER);
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ // In tethering mode, in the default configuration, an explicit request
+ // for a mobile network is also made.
+ verify(mUpstreamNetworkMonitor, times(1)).setTryCell(true);
+ // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_TETHERED
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI));
+
+ /////
+ // We do not currently emulate any upstream being found.
+ //
+ // This is why there are no calls to verify mNetd.tetherAddForward() or
+ // mNetd.ipfwdAddInterfaceForward().
+ /////
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.stopTethering(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).stopSoftAp();
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNetd);
+
+ // Emulate externally-visible WifiManager effects, when tethering mode
+ // is being torn down.
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME);
+ verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+ // interfaceSetCfg() called once for enabling and twice for disabling IPv4.
+ verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, times(1)).tetherStop();
+ verify(mNetd, times(1)).ipfwdDisableForwarding(TETHERING_NAME);
+ verify(mWifiManager, times(3)).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verifyNoMoreInteractions(mNetd);
+ verifyNoMoreInteractions(mWifiManager);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_WLAN_IFNAME));
+ }
+
+ // TODO: Test with and without interfaceStatusChanged().
+ @Test
+ public void failureEnablingIpForwarding() throws Exception {
+ when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
+ doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startTetheredHotspot(null);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNetd);
+
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // tethering mode is to be started.
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ // We verify get/set called three times here: twice for setup and once during
+ // teardown because all events happen over the course of the single
+ // dispatchAll() above. Note that once the IpServer IPv4 address config
+ // code is refactored the two calls during shutdown will revert to one.
+ verify(mNetd, times(3)).interfaceSetCfg(argThat(p -> TEST_WLAN_IFNAME.equals(p.ifName)));
+ verify(mNetd, times(1)).tetherInterfaceAdd(TEST_WLAN_IFNAME);
+ verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+ verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME),
+ anyString(), anyString());
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
+ // There are 3 IpServer state change event:
+ // STATE_AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE.
+ verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI));
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ // This is called, but will throw.
+ verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
+ // This never gets called because of the exception thrown above.
+ verify(mNetd, times(0)).tetherStartWithConfiguration(any());
+ // When the main state machine transitions to an error state it tells
+ // downstream interfaces, which causes us to tell Wi-Fi about the error
+ // so it can take down AP mode.
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME);
+ verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
+
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNetd);
+ }
+
+ private UserRestrictionActionListener makeUserRestrictionActionListener(
+ final Tethering tethering, final boolean currentDisallow, final boolean nextDisallow) {
+ final Bundle newRestrictions = new Bundle();
+ newRestrictions.putBoolean(UserManager.DISALLOW_CONFIG_TETHERING, nextDisallow);
+ when(mUserManager.getUserRestrictions()).thenReturn(newRestrictions);
+
+ final UserRestrictionActionListener ural =
+ new UserRestrictionActionListener(mUserManager, tethering, mNotificationUpdater);
+ ural.mDisallowTethering = currentDisallow;
+ return ural;
+ }
+
+ private void runUserRestrictionsChange(
+ boolean currentDisallow, boolean nextDisallow, boolean isTetheringActive,
+ int expectedInteractionsWithShowNotification) throws Exception {
+ final Tethering mockTethering = mock(Tethering.class);
+ when(mockTethering.isTetheringActive()).thenReturn(isTetheringActive);
+
+ final UserRestrictionActionListener ural =
+ makeUserRestrictionActionListener(mockTethering, currentDisallow, nextDisallow);
+ ural.onUserRestrictionsChanged();
+
+ verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification))
+ .notifyTetheringDisabledByRestriction();
+ verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+ }
+
+ @Test
+ public void testDisallowTetheringWhenTetheringIsNotActive() throws Exception {
+ final boolean isTetheringActive = false;
+ final boolean currDisallow = false;
+ final boolean nextDisallow = true;
+ final int expectedInteractionsWithShowNotification = 0;
+
+ runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testDisallowTetheringWhenTetheringIsActive() throws Exception {
+ final boolean isTetheringActive = true;
+ final boolean currDisallow = false;
+ final boolean nextDisallow = true;
+ final int expectedInteractionsWithShowNotification = 1;
+
+ runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testAllowTetheringWhenTetheringIsNotActive() throws Exception {
+ final boolean isTetheringActive = false;
+ final boolean currDisallow = true;
+ final boolean nextDisallow = false;
+ final int expectedInteractionsWithShowNotification = 0;
+
+ runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testAllowTetheringWhenTetheringIsActive() throws Exception {
+ final boolean isTetheringActive = true;
+ final boolean currDisallow = true;
+ final boolean nextDisallow = false;
+ final int expectedInteractionsWithShowNotification = 0;
+
+ runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testDisallowTetheringUnchanged() throws Exception {
+ final boolean isTetheringActive = true;
+ final int expectedInteractionsWithShowNotification = 0;
+ boolean currDisallow = true;
+ boolean nextDisallow = true;
+
+ runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive,
+ expectedInteractionsWithShowNotification);
+
+ currDisallow = false;
+ nextDisallow = false;
+
+ runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testUntetherUsbWhenRestrictionIsOn() {
+ // Start usb tethering and check that usb interface is tethered.
+ final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ runUsbTethering(upstreamState);
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+ assertTrue(mTethering.isTetheringActive());
+ assertEquals(0, mTethering.getActiveTetheringRequests().size());
+
+ final Tethering.UserRestrictionActionListener ural = makeUserRestrictionActionListener(
+ mTethering, false /* currentDisallow */, true /* nextDisallow */);
+
+ ural.onUserRestrictionsChanged();
+ mLooper.dispatchAll();
+
+ // Verify that restriction notification has showed to user.
+ verify(mNotificationUpdater, times(1)).notifyTetheringDisabledByRestriction();
+ // Verify that usb tethering has been disabled.
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ }
+
+ private class TestTetheringEventCallback extends ITetheringEventCallback.Stub {
+ private final ArrayList<Network> mActualUpstreams = new ArrayList<>();
+ private final ArrayList<TetheringConfigurationParcel> mTetheringConfigs =
+ new ArrayList<>();
+ private final ArrayList<TetherStatesParcel> mTetherStates = new ArrayList<>();
+ private final ArrayList<Integer> mOffloadStatus = new ArrayList<>();
+ private final ArrayList<List<TetheredClient>> mTetheredClients = new ArrayList<>();
+
+ // This function will remove the recorded callbacks, so it must be called once for
+ // each callback. If this is called after multiple callback, the order matters.
+ // onCallbackCreated counts as the first call to expectUpstreamChanged with
+ // @see onCallbackCreated.
+ public void expectUpstreamChanged(Network... networks) {
+ if (networks == null) {
+ assertNoUpstreamChangeCallback();
+ return;
+ }
+
+ final ArrayList<Network> expectedUpstreams =
+ new ArrayList<Network>(Arrays.asList(networks));
+ for (Network upstream : expectedUpstreams) {
+ // throws OOB if no expectations
+ assertEquals(mActualUpstreams.remove(0), upstream);
+ }
+ assertNoUpstreamChangeCallback();
+ }
+
+ // This function will remove the recorded callbacks, so it must be called once
+ // for each callback. If this is called after multiple callback, the order matters.
+ // onCallbackCreated counts as the first call to onConfigurationChanged with
+ // @see onCallbackCreated.
+ public void expectConfigurationChanged(TetheringConfigurationParcel... tetherConfigs) {
+ final ArrayList<TetheringConfigurationParcel> expectedTetherConfig =
+ new ArrayList<TetheringConfigurationParcel>(Arrays.asList(tetherConfigs));
+ for (TetheringConfigurationParcel config : expectedTetherConfig) {
+ // throws OOB if no expectations
+ final TetheringConfigurationParcel actualConfig = mTetheringConfigs.remove(0);
+ assertTetherConfigParcelEqual(actualConfig, config);
+ }
+ assertNoConfigChangeCallback();
+ }
+
+ public void expectOffloadStatusChanged(final int expectedStatus) {
+ assertOffloadStatusChangedCallback();
+ assertEquals(mOffloadStatus.remove(0), new Integer(expectedStatus));
+ }
+
+ public TetherStatesParcel pollTetherStatesChanged() {
+ assertStateChangeCallback();
+ return mTetherStates.remove(0);
+ }
+
+ public void expectTetheredClientChanged(List<TetheredClient> leases) {
+ assertFalse(mTetheredClients.isEmpty());
+ final List<TetheredClient> result = mTetheredClients.remove(0);
+ assertEquals(leases.size(), result.size());
+ assertTrue(leases.containsAll(result));
+ }
+
+ @Override
+ public void onUpstreamChanged(Network network) {
+ mActualUpstreams.add(network);
+ }
+
+ @Override
+ public void onConfigurationChanged(TetheringConfigurationParcel config) {
+ mTetheringConfigs.add(config);
+ }
+
+ @Override
+ public void onTetherStatesChanged(TetherStatesParcel states) {
+ mTetherStates.add(states);
+ }
+
+ @Override
+ public void onTetherClientsChanged(List<TetheredClient> clients) {
+ mTetheredClients.add(clients);
+ }
+
+ @Override
+ public void onOffloadStatusChanged(final int status) {
+ mOffloadStatus.add(status);
+ }
+
+ @Override
+ public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
+ mActualUpstreams.add(parcel.upstreamNetwork);
+ mTetheringConfigs.add(parcel.config);
+ mTetherStates.add(parcel.states);
+ mOffloadStatus.add(parcel.offloadStatus);
+ mTetheredClients.add(parcel.tetheredClients);
+ }
+
+ @Override
+ public void onCallbackStopped(int errorCode) { }
+
+ public void assertNoUpstreamChangeCallback() {
+ assertTrue(mActualUpstreams.isEmpty());
+ }
+
+ public void assertNoConfigChangeCallback() {
+ assertTrue(mTetheringConfigs.isEmpty());
+ }
+
+ public void assertNoStateChangeCallback() {
+ assertTrue(mTetherStates.isEmpty());
+ }
+
+ public void assertStateChangeCallback() {
+ assertFalse(mTetherStates.isEmpty());
+ }
+
+ public void assertOffloadStatusChangedCallback() {
+ assertFalse(mOffloadStatus.isEmpty());
+ }
+
+ public void assertNoCallback() {
+ assertNoUpstreamChangeCallback();
+ assertNoConfigChangeCallback();
+ assertNoStateChangeCallback();
+ assertTrue(mTetheredClients.isEmpty());
+ }
+
+ private void assertTetherConfigParcelEqual(@NonNull TetheringConfigurationParcel actual,
+ @NonNull TetheringConfigurationParcel expect) {
+ assertEquals(actual.subId, expect.subId);
+ assertArrayEquals(actual.tetherableUsbRegexs, expect.tetherableUsbRegexs);
+ assertArrayEquals(actual.tetherableWifiRegexs, expect.tetherableWifiRegexs);
+ assertArrayEquals(actual.tetherableBluetoothRegexs, expect.tetherableBluetoothRegexs);
+ assertEquals(actual.isDunRequired, expect.isDunRequired);
+ assertEquals(actual.chooseUpstreamAutomatically, expect.chooseUpstreamAutomatically);
+ assertArrayEquals(actual.preferredUpstreamIfaceTypes,
+ expect.preferredUpstreamIfaceTypes);
+ assertArrayEquals(actual.legacyDhcpRanges, expect.legacyDhcpRanges);
+ assertArrayEquals(actual.defaultIPv4DNS, expect.defaultIPv4DNS);
+ assertEquals(actual.enableLegacyDhcpServer, expect.enableLegacyDhcpServer);
+ assertArrayEquals(actual.provisioningApp, expect.provisioningApp);
+ assertEquals(actual.provisioningAppNoUi, expect.provisioningAppNoUi);
+ assertEquals(actual.provisioningCheckPeriod, expect.provisioningCheckPeriod);
+ }
+ }
+
+ private void assertTetherStatesNotNullButEmpty(final TetherStatesParcel parcel) {
+ assertFalse(parcel == null);
+ assertEquals(0, parcel.availableList.length);
+ assertEquals(0, parcel.tetheredList.length);
+ assertEquals(0, parcel.localOnlyList.length);
+ assertEquals(0, parcel.erroredIfaceList.length);
+ assertEquals(0, parcel.lastErrorList.length);
+ MiscAsserts.assertFieldCountEquals(5, TetherStatesParcel.class);
+ }
+
+ @Test
+ public void testRegisterTetheringEventCallback() throws Exception {
+ TestTetheringEventCallback callback = new TestTetheringEventCallback();
+ TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
+ final TetheringInterface wifiIface = new TetheringInterface(
+ TETHERING_WIFI, TEST_WLAN_IFNAME);
+
+ // 1. Register one callback before running any tethering.
+ mTethering.registerTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ callback.expectTetheredClientChanged(Collections.emptyList());
+ callback.expectUpstreamChanged(new Network[] {null});
+ callback.expectConfigurationChanged(
+ mTethering.getTetheringConfiguration().toStableParcelable());
+ TetherStatesParcel tetherState = callback.pollTetherStatesChanged();
+ assertTetherStatesNotNullButEmpty(tetherState);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ // 2. Enable wifi tethering.
+ UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ initTetheringUpstream(upstreamState);
+ when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
+
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
+ callback.expectUpstreamChanged(upstreamState.network);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
+
+ // 3. Register second callback.
+ mTethering.registerTetheringEventCallback(callback2);
+ mLooper.dispatchAll();
+ callback2.expectTetheredClientChanged(Collections.emptyList());
+ callback2.expectUpstreamChanged(upstreamState.network);
+ callback2.expectConfigurationChanged(
+ mTethering.getTetheringConfiguration().toStableParcelable());
+ tetherState = callback2.pollTetherStatesChanged();
+ assertEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
+ callback2.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
+
+ // 4. Unregister first callback and disable wifi tethering
+ mTethering.unregisterTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ mTethering.stopTethering(TETHERING_WIFI);
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ tetherState = callback2.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
+ mLooper.dispatchAll();
+ callback2.expectUpstreamChanged(new Network[] {null});
+ callback2.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ callback.assertNoCallback();
+ }
+
+ @Test
+ public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
+ final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ TestTetheringEventCallback callback = new TestTetheringEventCallback();
+ mTethering.registerTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+
+ // 1. Offload fail if no OffloadConfig.
+ initOffloadConfiguration(false /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
+ 0 /* defaultDisabled */);
+ runUsbTethering(upstreamState);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
+ runStopUSBTethering();
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ reset(mUsbManager);
+ // 2. Offload fail if no OffloadControl.
+ initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_NONE,
+ 0 /* defaultDisabled */);
+ runUsbTethering(upstreamState);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
+ runStopUSBTethering();
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ reset(mUsbManager);
+ // 3. Offload fail if disabled by settings.
+ initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
+ 1 /* defaultDisabled */);
+ runUsbTethering(upstreamState);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
+ runStopUSBTethering();
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ }
+
+ private void runStopUSBTethering() {
+ mTethering.stopTethering(TETHERING_USB);
+ mLooper.dispatchAll();
+ mTethering.interfaceRemoved(TEST_USB_IFNAME);
+ mLooper.dispatchAll();
+ }
+
+ private void initOffloadConfiguration(final boolean offloadConfig,
+ @OffloadHardwareInterface.OffloadHalVersion final int offloadControlVersion,
+ final int defaultDisabled) {
+ when(mOffloadHardwareInterface.initOffloadConfig()).thenReturn(offloadConfig);
+ when(mOffloadHardwareInterface.initOffloadControl(any())).thenReturn(offloadControlVersion);
+ when(mOffloadHardwareInterface.getDefaultTetherOffloadDisabled()).thenReturn(
+ defaultDisabled);
+ }
+
+ @Test
+ public void testMultiSimAware() throws Exception {
+ final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
+ assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.activeDataSubId);
+
+ final int fakeSubId = 1234;
+ mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
+ final TetheringConfiguration newConfig = mTethering.getTetheringConfiguration();
+ assertEquals(fakeSubId, newConfig.activeDataSubId);
+ verify(mNotificationUpdater, times(1)).onActiveDataSubscriptionIdChanged(eq(fakeSubId));
+ }
+
+ @Test
+ public void testNoDuplicatedEthernetRequest() throws Exception {
+ final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
+ when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+ mLooper.dispatchAll();
+ verify(mEm, times(1)).requestTetheredInterface(any(), any());
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+ mLooper.dispatchAll();
+ verifyNoMoreInteractions(mEm);
+ mTethering.stopTethering(TETHERING_ETHERNET);
+ mLooper.dispatchAll();
+ verify(mockRequest, times(1)).release();
+ mTethering.stopTethering(TETHERING_ETHERNET);
+ mLooper.dispatchAll();
+ verifyNoMoreInteractions(mEm);
+ }
+
+ private void workingWifiP2pGroupOwner(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ }
+ sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+
+ verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+ verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
+ verify(mNetd, times(1)).tetherStartWithConfiguration(any());
+ verifyNoMoreInteractions(mNetd);
+ verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
+ verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
+
+ assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
+
+ // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
+ // is being removed.
+ sendWifiP2pConnectionChanged(false, true, TEST_P2P_IFNAME);
+ mTethering.interfaceRemoved(TEST_P2P_IFNAME);
+
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ verify(mNetd, times(1)).tetherInterfaceRemove(TEST_P2P_IFNAME);
+ verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME);
+ // interfaceSetCfg() called once for enabling and twice for disabling IPv4.
+ verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, times(1)).tetherStop();
+ verify(mNetd, times(1)).ipfwdDisableForwarding(TETHERING_NAME);
+ verify(mUpstreamNetworkMonitor, never()).getCurrentPreferredUpstream();
+ verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any());
+ verifyNoMoreInteractions(mNetd);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
+ }
+
+ private void workingWifiP2pGroupClient(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ }
+ sendWifiP2pConnectionChanged(true, false, TEST_P2P_IFNAME);
+
+ verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME);
+ verify(mNetd, never()).networkAddInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME);
+ verify(mNetd, never()).ipfwdEnableForwarding(TETHERING_NAME);
+ verify(mNetd, never()).tetherStartWithConfiguration(any());
+
+ // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
+ // is being removed.
+ sendWifiP2pConnectionChanged(false, false, TEST_P2P_IFNAME);
+ mTethering.interfaceRemoved(TEST_P2P_IFNAME);
+
+ verify(mNetd, never()).tetherApplyDnsInterfaces();
+ verify(mNetd, never()).tetherInterfaceRemove(TEST_P2P_IFNAME);
+ verify(mNetd, never()).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME);
+ verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, never()).tetherStop();
+ verify(mNetd, never()).ipfwdDisableForwarding(TETHERING_NAME);
+ verifyNoMoreInteractions(mNetd);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
+ }
+
+ @Test
+ public void workingWifiP2pGroupOwnerWithIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwner(true);
+ }
+
+ @Test
+ public void workingWifiP2pGroupOwnerSansIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwner(false);
+ }
+
+ private void workingWifiP2pGroupOwnerLegacyMode(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ // change to legacy mode and update tethering information by chaning SIM
+ when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
+ .thenReturn(new String[]{});
+ final int fakeSubId = 1234;
+ mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
+
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ }
+ sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+
+ verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME);
+ verify(mNetd, never()).networkAddInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME);
+ verify(mNetd, never()).ipfwdEnableForwarding(TETHERING_NAME);
+ verify(mNetd, never()).tetherStartWithConfiguration(any());
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
+ }
+ @Test
+ public void workingWifiP2pGroupOwnerLegacyModeWithIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwnerLegacyMode(true);
+ }
+
+ @Test
+ public void workingWifiP2pGroupOwnerLegacyModeSansIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwnerLegacyMode(false);
+ }
+
+ @Test
+ public void workingWifiP2pGroupClientWithIfaceChanged() throws Exception {
+ workingWifiP2pGroupClient(true);
+ }
+
+ @Test
+ public void workingWifiP2pGroupClientSansIfaceChanged() throws Exception {
+ workingWifiP2pGroupClient(false);
+ }
+
+ private void setDataSaverEnabled(boolean enabled) {
+ final int status = enabled ? RESTRICT_BACKGROUND_STATUS_ENABLED
+ : RESTRICT_BACKGROUND_STATUS_DISABLED;
+ doReturn(status).when(mCm).getRestrictBackgroundStatus();
+
+ final Intent intent = new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED);
+ mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
+ }
+
+ @Test
+ public void testDataSaverChanged() {
+ // Start Tethering.
+ final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ runUsbTethering(upstreamState);
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+ // Data saver is ON.
+ setDataSaverEnabled(true);
+ // Verify that tethering should be disabled.
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ mTethering.interfaceRemoved(TEST_USB_IFNAME);
+ mLooper.dispatchAll();
+ assertEquals(mTethering.getTetheredIfaces(), new String[0]);
+ reset(mUsbManager);
+
+ runUsbTethering(upstreamState);
+ // Verify that user can start tethering again without turning OFF data saver.
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+
+ // If data saver is keep ON with change event, tethering should not be OFF this time.
+ setDataSaverEnabled(true);
+ verify(mUsbManager, times(0)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+
+ // If data saver is turned OFF, it should not change tethering.
+ setDataSaverEnabled(false);
+ verify(mUsbManager, times(0)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+ }
+
+ private static <T> void assertContains(Collection<T> collection, T element) {
+ assertTrue(element + " not found in " + collection, collection.contains(element));
+ }
+
+ private class ResultListener extends IIntResultListener.Stub {
+ private final int mExpectedResult;
+ private boolean mHasResult = false;
+ ResultListener(final int expectedResult) {
+ mExpectedResult = expectedResult;
+ }
+
+ @Override
+ public void onResult(final int resultCode) {
+ mHasResult = true;
+ if (resultCode != mExpectedResult) {
+ fail("expected result: " + mExpectedResult + " but actual result: " + resultCode);
+ }
+ }
+
+ public void assertHasResult() {
+ if (!mHasResult) fail("No callback result");
+ }
+ }
+
+ @Test
+ public void testMultipleStartTethering() throws Exception {
+ final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
+ final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
+ final String serverAddr = "192.168.20.1";
+ final ResultListener firstResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+
+ // Enable USB tethering and check that Tethering starts USB.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult);
+ mLooper.dispatchAll();
+ firstResult.assertHasResult();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ verifyNoMoreInteractions(mUsbManager);
+
+ // Enable USB tethering again with the same request and expect no change to USB.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult);
+ mLooper.dispatchAll();
+ secondResult.assertHasResult();
+ verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ reset(mUsbManager);
+
+ // Enable USB tethering with a different request and expect that USB is stopped and
+ // started.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult);
+ mLooper.dispatchAll();
+ thirdResult.assertHasResult();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+ // Expect that when USB comes up, the DHCP server is configured with the requested address.
+ mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
+ }
+
+ @Test
+ public void testRequestStaticIp() throws Exception {
+ final LinkAddress serverLinkAddr = new LinkAddress("192.168.0.123/24");
+ final LinkAddress clientLinkAddr = new LinkAddress("192.168.0.42/24");
+ final String serverAddr = "192.168.0.123";
+ final int clientAddrParceled = 0xc0a8002a;
+ final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
+ ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
+ mLooper.dispatchAll();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
+ verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(),
+ any());
+ final DhcpServingParamsParcel params = dhcpParamsCaptor.getValue();
+ assertEquals(serverAddr, intToInet4AddressHTH(params.serverAddr).getHostAddress());
+ assertEquals(24, params.serverAddrPrefixLength);
+ assertEquals(clientAddrParceled, params.singleClientAddr);
+ }
+
+ @Test
+ public void testUpstreamNetworkChanged() {
+ final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
+ mTetheringDependencies.mUpstreamNetworkMonitorSM;
+ final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ initTetheringUpstream(upstreamState);
+ stateMachine.chooseUpstreamType(true);
+
+ verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(eq(upstreamState.network));
+ verify(mNotificationUpdater, times(1)).onUpstreamCapabilitiesChanged(any());
+ }
+
+ @Test
+ public void testUpstreamCapabilitiesChanged() {
+ final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
+ mTetheringDependencies.mUpstreamNetworkMonitorSM;
+ final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+ initTetheringUpstream(upstreamState);
+ stateMachine.chooseUpstreamType(true);
+
+ stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+ // Should have two onUpstreamCapabilitiesChanged().
+ // One is called by reportUpstreamChanged(). One is called by EVENT_ON_CAPABILITIES.
+ verify(mNotificationUpdater, times(2)).onUpstreamCapabilitiesChanged(any());
+ reset(mNotificationUpdater);
+
+ // Verify that onUpstreamCapabilitiesChanged won't be called if not current upstream network
+ // capabilities changed.
+ final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
+ upstreamState.linkProperties, upstreamState.networkCapabilities,
+ new Network(WIFI_NETID));
+ stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
+ verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
+ }
+
+ @Test
+ public void testDumpTetheringLog() throws Exception {
+ final FileDescriptor mockFd = mock(FileDescriptor.class);
+ final PrintWriter mockPw = mock(PrintWriter.class);
+ runUsbTethering(null);
+ mLooper.startAutoDispatch();
+ mTethering.dump(mockFd, mockPw, new String[0]);
+ verify(mConfig).dump(any());
+ verify(mEntitleMgr).dump(any());
+ verify(mOffloadCtrl).dump(any());
+ mLooper.stopAutoDispatch();
+ }
+
+ @Test
+ public void testExemptFromEntitlementCheck() throws Exception {
+ setupForRequiredProvisioning();
+ final TetheringRequestParcel wifiNotExemptRequest =
+ createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
+ CONNECTIVITY_SCOPE_GLOBAL);
+ mTethering.startTethering(wifiNotExemptRequest, null);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
+ verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
+ assertFalse(mEntitleMgr.isCellularUpstreamPermitted());
+ mTethering.stopTethering(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
+ reset(mEntitleMgr);
+
+ setupForRequiredProvisioning();
+ final TetheringRequestParcel wifiExemptRequest =
+ createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
+ CONNECTIVITY_SCOPE_GLOBAL);
+ mTethering.startTethering(wifiExemptRequest, null);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
+ verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
+ assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
+ mTethering.stopTethering(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
+ reset(mEntitleMgr);
+
+ // If one app enables tethering without provisioning check first, then another app enables
+ // tethering of the same type but does not disable the provisioning check.
+ setupForRequiredProvisioning();
+ mTethering.startTethering(wifiExemptRequest, null);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
+ verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
+ assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
+ reset(mEntitleMgr);
+ setupForRequiredProvisioning();
+ mTethering.startTethering(wifiNotExemptRequest, null);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
+ verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
+ assertFalse(mEntitleMgr.isCellularUpstreamPermitted());
+ mTethering.stopTethering(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
+ reset(mEntitleMgr);
+ }
+
+ private void setupForRequiredProvisioning() {
+ // Produce some acceptable looking provision app setting if requested.
+ when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+ .thenReturn(PROVISIONING_APP_NAME);
+ when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui))
+ .thenReturn(PROVISIONING_NO_UI_APP_NAME);
+ // Act like the CarrierConfigManager is present and ready unless told otherwise.
+ when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
+ .thenReturn(mCarrierConfigManager);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
+ mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+ mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+ sendConfigurationChanged();
+ }
+
+ private static UpstreamNetworkState buildV4UpstreamState(final LinkAddress address,
+ final Network network, final String iface, final int transportType) {
+ final LinkProperties prop = new LinkProperties();
+ prop.setInterfaceName(iface);
+
+ prop.addLinkAddress(address);
+
+ final NetworkCapabilities capabilities = new NetworkCapabilities()
+ .addTransportType(transportType);
+ return new UpstreamNetworkState(prop, capabilities, network);
+ }
+
+ private void updateV4Upstream(final LinkAddress ipv4Address, final Network network,
+ final String iface, final int transportType) {
+ final UpstreamNetworkState upstream = buildV4UpstreamState(ipv4Address, network, iface,
+ transportType);
+ mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
+ Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
+ UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
+ 0,
+ upstream);
+ mLooper.dispatchAll();
+ }
+
+ @Test
+ public void testHandleIpConflict() throws Exception {
+ final Network wifiNetwork = new Network(200);
+ final Network[] allNetworks = { wifiNetwork };
+ doReturn(allNetworks).when(mCm).getAllNetworks();
+ runUsbTethering(null);
+ final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
+ ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+ verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture());
+ final String ipv4Address = ifaceConfigCaptor.getValue().ipv4Addr;
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ reset(mNetd, mUsbManager);
+
+ // Cause a prefix conflict by assigning a /30 out of the downstream's /24 to the upstream.
+ updateV4Upstream(new LinkAddress(InetAddresses.parseNumericAddress(ipv4Address), 30),
+ wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI);
+ // verify turn off usb tethering
+ verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ mTethering.interfaceRemoved(TEST_USB_IFNAME);
+ mLooper.dispatchAll();
+ // verify restart usb tethering
+ verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ }
+
+ @Test
+ public void testNoAddressAvailable() throws Exception {
+ final Network wifiNetwork = new Network(200);
+ final Network btNetwork = new Network(201);
+ final Network mobileNetwork = new Network(202);
+ final Network[] allNetworks = { wifiNetwork, btNetwork, mobileNetwork };
+ doReturn(allNetworks).when(mCm).getAllNetworks();
+ runUsbTethering(null);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+ reset(mUsbManager);
+ final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
+ when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
+ final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
+ ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+ mLooper.dispatchAll();
+ verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
+ TetheredInterfaceCallback ethCallback = callbackCaptor.getValue();
+ ethCallback.onAvailable(TEST_ETH_IFNAME);
+ mLooper.dispatchAll();
+ reset(mUsbManager, mEm);
+
+ updateV4Upstream(new LinkAddress("192.168.0.100/16"), wifiNetwork, TEST_WIFI_IFNAME,
+ TRANSPORT_WIFI);
+ updateV4Upstream(new LinkAddress("172.16.0.0/12"), btNetwork, TEST_BT_IFNAME,
+ TRANSPORT_BLUETOOTH);
+ updateV4Upstream(new LinkAddress("10.0.0.0/8"), mobileNetwork, TEST_MOBILE_IFNAME,
+ TRANSPORT_CELLULAR);
+
+ mLooper.dispatchAll();
+ // verify turn off usb tethering
+ verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ // verify turn off ethernet tethering
+ verify(mockRequest).release();
+ mTethering.interfaceRemoved(TEST_USB_IFNAME);
+ ethCallback.onUnavailable();
+ mLooper.dispatchAll();
+ // verify restart usb tethering
+ verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ // verify restart ethernet tethering
+ verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
+ ethCallback = callbackCaptor.getValue();
+ ethCallback.onAvailable(TEST_ETH_IFNAME);
+
+ reset(mUsbManager, mEm);
+ when(mNetd.interfaceGetList())
+ .thenReturn(new String[] {
+ TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME,
+ TEST_NCM_IFNAME, TEST_ETH_IFNAME});
+
+ mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ assertContains(Arrays.asList(mTethering.getTetherableIfacesForTest()), TEST_USB_IFNAME);
+ assertContains(Arrays.asList(mTethering.getTetherableIfacesForTest()), TEST_ETH_IFNAME);
+ assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastErrorForTest(TEST_USB_IFNAME));
+ assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastErrorForTest(TEST_ETH_IFNAME));
+ }
+
+ @Test
+ public void testProvisioningNeededButUnavailable() throws Exception {
+ assertTrue(mTethering.isTetheringSupported());
+ verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
+
+ setupForRequiredProvisioning();
+ assertTrue(mTethering.isTetheringSupported());
+ verify(mPackageManager).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
+ reset(mPackageManager);
+
+ doThrow(PackageManager.NameNotFoundException.class).when(mPackageManager).getPackageInfo(
+ PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
+ setupForRequiredProvisioning();
+ assertFalse(mTethering.isTetheringSupported());
+ verify(mPackageManager).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
+ }
+
+ @Test
+ public void testUpdateConnectedClients() throws Exception {
+ TestTetheringEventCallback callback = new TestTetheringEventCallback();
+ runAsShell(NETWORK_SETTINGS, () -> {
+ mTethering.registerTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ });
+ callback.expectTetheredClientChanged(Collections.emptyList());
+
+ IDhcpEventCallbacks eventCallbacks;
+ final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+ ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+ // Run local only tethering.
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+ any(), dhcpEventCbsCaptor.capture());
+ eventCallbacks = dhcpEventCbsCaptor.getValue();
+ // Update lease for local only tethering.
+ final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
+ final ArrayList<DhcpLeaseParcelable> p2pLeases = new ArrayList<>();
+ p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24,
+ Long.MAX_VALUE, "test1"));
+ notifyDhcpLeasesChanged(p2pLeases, eventCallbacks);
+ final List<TetheredClient> clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P);
+ callback.expectTetheredClientChanged(clients);
+ reset(mDhcpServer);
+
+ // Run wifi tethering.
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+ any(), dhcpEventCbsCaptor.capture());
+ eventCallbacks = dhcpEventCbsCaptor.getValue();
+ // Update mac address from softAp callback before getting dhcp lease.
+ final ArrayList<WifiClient> wifiClients = new ArrayList<>();
+ final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
+ final WifiClient testClient = mock(WifiClient.class);
+ when(testClient.getMacAddress()).thenReturn(testMac2);
+ wifiClients.add(testClient);
+ mSoftApCallback.onConnectedClientsChanged(wifiClients);
+ final TetheredClient noAddrClient = new TetheredClient(testMac2,
+ Collections.emptyList() /* addresses */, TETHERING_WIFI);
+ clients.add(noAddrClient);
+ callback.expectTetheredClientChanged(clients);
+
+ // Update dhcp lease for wifi tethering.
+ clients.remove(noAddrClient);
+ final ArrayList<DhcpLeaseParcelable> wifiLeases = new ArrayList<>();
+ wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24,
+ Long.MAX_VALUE, "test2"));
+ notifyDhcpLeasesChanged(wifiLeases, eventCallbacks);
+ clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI));
+ callback.expectTetheredClientChanged(clients);
+
+ // Test onStarted callback that register second callback when tethering is running.
+ TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
+ runAsShell(NETWORK_SETTINGS, () -> {
+ mTethering.registerTetheringEventCallback(callback2);
+ mLooper.dispatchAll();
+ });
+ callback2.expectTetheredClientChanged(clients);
+ }
+
+ private void notifyDhcpLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables,
+ IDhcpEventCallbacks callback) throws Exception {
+ callback.onLeasesChanged(leaseParcelables);
+ mLooper.dispatchAll();
+ }
+
+ private List<TetheredClient> toTetheredClients(List<DhcpLeaseParcelable> leaseParcelables,
+ int type) throws Exception {
+ final ArrayList<TetheredClient> leases = new ArrayList<>();
+ for (DhcpLeaseParcelable lease : leaseParcelables) {
+ final LinkAddress address = new LinkAddress(
+ intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
+ 0 /* flags */, RT_SCOPE_UNIVERSE /* as per RFC6724#3.2 */,
+ lease.expTime /* deprecationTime */, lease.expTime /* expirationTime */);
+
+ final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr);
+
+ final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname);
+ leases.add(new TetheredClient(
+ macAddress,
+ Collections.singletonList(addressInfo),
+ type));
+ }
+
+ return leases;
+ }
+
+ private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId,
+ final MacAddress hwAddr, final String netAddr, final int prefixLength,
+ final long expTime, final String hostname) throws Exception {
+ final DhcpLeaseParcelable lease = new DhcpLeaseParcelable();
+ lease.clientId = clientId.getBytes();
+ lease.hwAddr = hwAddr.toByteArray();
+ lease.netAddr = inet4AddressToIntHTH(
+ (Inet4Address) InetAddresses.parseNumericAddress(netAddr));
+ lease.prefixLength = prefixLength;
+ lease.expTime = expTime;
+ lease.hostname = hostname;
+
+ return lease;
+ }
+
+ @Test
+ public void testBluetoothTethering() throws Exception {
+ final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+ mLooper.dispatchAll();
+ verifySetBluetoothTethering(true);
+ result.assertHasResult();
+
+ mTethering.interfaceAdded(TEST_BT_IFNAME);
+ mLooper.dispatchAll();
+
+ mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
+ mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
+ final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mLooper.dispatchAll();
+ tetherResult.assertHasResult();
+
+ verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME);
+ verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+ verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+ anyString(), anyString());
+ verify(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
+ verify(mNetd).tetherStartWithConfiguration(any());
+ verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+ anyString(), anyString());
+ verifyNoMoreInteractions(mNetd);
+ reset(mNetd);
+
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ mTethering.stopTethering(TETHERING_BLUETOOTH);
+ mLooper.dispatchAll();
+ final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.untether(TEST_BT_IFNAME, untetherResult);
+ mLooper.dispatchAll();
+ untetherResult.assertHasResult();
+ verifySetBluetoothTethering(false);
+
+ verify(mNetd).tetherApplyDnsInterfaces();
+ verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME);
+ verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+ verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ verify(mNetd).tetherStop();
+ verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME);
+ verifyNoMoreInteractions(mNetd);
+ }
+
+ private void verifySetBluetoothTethering(final boolean enable) {
+ final ArgumentCaptor<ServiceListener> listenerCaptor =
+ ArgumentCaptor.forClass(ServiceListener.class);
+ verify(mBluetoothAdapter).isEnabled();
+ verify(mBluetoothAdapter).getProfileProxy(eq(mServiceContext), listenerCaptor.capture(),
+ eq(BluetoothProfile.PAN));
+ final ServiceListener listener = listenerCaptor.getValue();
+ when(mBluetoothPan.isTetheringOn()).thenReturn(enable);
+ listener.onServiceConnected(BluetoothProfile.PAN, mBluetoothPan);
+ verify(mBluetoothPan).setBluetoothTethering(enable);
+ verify(mBluetoothPan).isTetheringOn();
+ verify(mBluetoothAdapter).closeProfileProxy(eq(BluetoothProfile.PAN), eq(mBluetoothPan));
+ verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan);
+ reset(mBluetoothAdapter, mBluetoothPan);
+ }
+
+ // TODO: Test that a request for hotspot mode doesn't interfere with an
+ // already operating tethering mode interface.
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
new file mode 100644
index 0000000..ce4ba85
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.networkstack.tethering;
+
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static com.android.networkstack.tethering.UpstreamNetworkMonitor.TYPE_NONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IConnectivityManager;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
+import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpstreamNetworkMonitorTest {
+ private static final int EVENT_UNM_UPDATE = 1;
+
+ private static final boolean INCLUDES = true;
+ private static final boolean EXCLUDES = false;
+
+ private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_INTERNET).build();
+ private static final NetworkCapabilities DUN_CAPABILITIES = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_DUN).build();
+ private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET).build();
+
+ @Mock private Context mContext;
+ @Mock private EntitlementManager mEntitleMgr;
+ @Mock private IConnectivityManager mCS;
+ @Mock private SharedLog mLog;
+
+ private TestStateMachine mSM;
+ private TestConnectivityManager mCM;
+ private UpstreamNetworkMonitor mUNM;
+
+ private final TestLooper mLooper = new TestLooper();
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ reset(mContext);
+ reset(mCS);
+ reset(mLog);
+ when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+
+ mCM = spy(new TestConnectivityManager(mContext, mCS));
+ when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mCM);
+ mSM = new TestStateMachine(mLooper.getLooper());
+ mUNM = new UpstreamNetworkMonitor(mContext, mSM, mLog, EVENT_UNM_UPDATE);
+ }
+
+ @After public void tearDown() throws Exception {
+ if (mSM != null) {
+ mSM.quit();
+ mSM = null;
+ }
+ }
+
+ @Test
+ public void testStopWithoutStartIsNonFatal() {
+ mUNM.stop();
+ mUNM.stop();
+ mUNM.stop();
+ }
+
+ @Test
+ public void testDoesNothingBeforeTrackDefaultAndStarted() throws Exception {
+ assertTrue(mCM.hasNoCallbacks());
+ assertFalse(mUNM.mobileNetworkRequested());
+
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ assertTrue(mCM.hasNoCallbacks());
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
+ assertTrue(mCM.hasNoCallbacks());
+ }
+
+ @Test
+ public void testDefaultNetworkIsTracked() throws Exception {
+ assertTrue(mCM.hasNoCallbacks());
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+
+ mUNM.startObserveAllNetworks();
+ assertEquals(1, mCM.mTrackingDefault.size());
+
+ mUNM.stop();
+ assertTrue(mCM.onlyHasDefaultCallbacks());
+ }
+
+ @Test
+ public void testListensForAllNetworks() throws Exception {
+ assertTrue(mCM.mListening.isEmpty());
+
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+ mUNM.startObserveAllNetworks();
+ assertFalse(mCM.mListening.isEmpty());
+ assertTrue(mCM.isListeningForAll());
+
+ mUNM.stop();
+ assertTrue(mCM.onlyHasDefaultCallbacks());
+ }
+
+ @Test
+ public void testCallbacksRegistered() {
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+ // Verify the fired default request matches expectation.
+ final ArgumentCaptor<NetworkRequest> requestCaptor =
+ ArgumentCaptor.forClass(NetworkRequest.class);
+ verify(mCM, times(1)).requestNetwork(
+ requestCaptor.capture(), any(NetworkCallback.class), any(Handler.class));
+ // For R- devices, Tethering will invoke this function in 2 cases, one is to
+ // request mobile network, the other is to track system default network. Verify
+ // the request is the one tracks default network.
+ assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue()));
+
+ mUNM.startObserveAllNetworks();
+ verify(mCM, times(1)).registerNetworkCallback(
+ any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
+
+ mUNM.stop();
+ verify(mCM, times(1)).unregisterNetworkCallback(any(NetworkCallback.class));
+ }
+
+ @Test
+ public void testRequestsMobileNetwork() throws Exception {
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.startObserveAllNetworks();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.setTryCell(true);
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI);
+ assertFalse(isDunRequested());
+
+ mUNM.stop();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertTrue(mCM.hasNoCallbacks());
+ }
+
+ @Test
+ public void testDuplicateMobileRequestsIgnored() throws Exception {
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.startObserveAllNetworks();
+ verify(mCM, times(1)).registerNetworkCallback(
+ any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ mUNM.setTryCell(true);
+ verify(mCM, times(1)).requestNetwork(
+ any(NetworkRequest.class), anyInt(), anyInt(), any(Handler.class),
+ any(NetworkCallback.class));
+
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
+ assertTrue(isDunRequested());
+
+ // Try a few things that must not result in any state change.
+ mUNM.setTryCell(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ mUNM.setTryCell(true);
+
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
+ assertTrue(isDunRequested());
+
+ mUNM.stop();
+ verify(mCM, times(2)).unregisterNetworkCallback(any(NetworkCallback.class));
+
+ verifyNoMoreInteractions(mCM);
+ }
+
+ @Test
+ public void testRequestsDunNetwork() throws Exception {
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.startObserveAllNetworks();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.mRequested.size());
+
+ mUNM.setTryCell(true);
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
+ assertTrue(isDunRequested());
+
+ mUNM.stop();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertTrue(mCM.hasNoCallbacks());
+ }
+
+ @Test
+ public void testUpdateMobileRequiresDun() throws Exception {
+ mUNM.startObserveAllNetworks();
+
+ // Test going from no-DUN to DUN correctly re-registers callbacks.
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
+ mUNM.setTryCell(true);
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI);
+ assertFalse(isDunRequested());
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
+ assertTrue(isDunRequested());
+
+ // Test going from DUN to no-DUN correctly re-registers callbacks.
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI);
+ assertFalse(isDunRequested());
+
+ mUNM.stop();
+ assertFalse(mUNM.mobileNetworkRequested());
+ }
+
+ @Test
+ public void testSelectPreferredUpstreamType() throws Exception {
+ final Collection<Integer> preferredTypes = new ArrayList<>();
+ preferredTypes.add(TYPE_WIFI);
+
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+ mUNM.startObserveAllNetworks();
+ // There are no networks, so there is nothing to select.
+ assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
+ wifiAgent.fakeConnect();
+ mLooper.dispatchAll();
+ // WiFi is up, we should prefer it.
+ assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+ wifiAgent.fakeDisconnect();
+ mLooper.dispatchAll();
+ // There are no networks, so there is nothing to select.
+ assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
+ cellAgent.fakeConnect();
+ mLooper.dispatchAll();
+ assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ preferredTypes.add(TYPE_MOBILE_DUN);
+ // This is coupled with preferred types in TetheringConfiguration.
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ // DUN is available, but only use regular cell: no upstream selected.
+ assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+ preferredTypes.remove(TYPE_MOBILE_DUN);
+ // No WiFi, but our preferred flavour of cell is up.
+ preferredTypes.add(TYPE_MOBILE_HIPRI);
+ // This is coupled with preferred types in TetheringConfiguration.
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
+ assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
+ mUNM.selectPreferredUpstreamType(preferredTypes));
+ // mobile is not permitted, we should not use HIPRI.
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
+ assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
+ mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ wifiAgent.fakeConnect();
+ mLooper.dispatchAll();
+ // WiFi is up, and we should prefer it over cell.
+ assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ preferredTypes.remove(TYPE_MOBILE_HIPRI);
+ preferredTypes.add(TYPE_MOBILE_DUN);
+ // This is coupled with preferred types in TetheringConfiguration.
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES);
+ dunAgent.fakeConnect();
+ mLooper.dispatchAll();
+
+ // WiFi is still preferred.
+ assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+ // WiFi goes down, cell and DUN are still up but only DUN is preferred.
+ wifiAgent.fakeDisconnect();
+ mLooper.dispatchAll();
+ assertSatisfiesLegacyType(TYPE_MOBILE_DUN,
+ mUNM.selectPreferredUpstreamType(preferredTypes));
+ // mobile is not permitted, we should not use DUN.
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
+ assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+ // mobile change back to permitted, DUN should come back
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ assertSatisfiesLegacyType(TYPE_MOBILE_DUN,
+ mUNM.selectPreferredUpstreamType(preferredTypes));
+ }
+
+ @Test
+ public void testGetCurrentPreferredUpstream() throws Exception {
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+ mUNM.startObserveAllNetworks();
+ mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
+ mUNM.setTryCell(true);
+
+ // [0] Mobile connects, DUN not required -> mobile selected.
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
+ cellAgent.fakeConnect();
+ mCM.makeDefaultNetwork(cellAgent);
+ mLooper.dispatchAll();
+ assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(0, mCM.mRequested.size());
+
+ // [1] Mobile connects but not permitted -> null selected
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
+ assertEquals(null, mUNM.getCurrentPreferredUpstream());
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ assertEquals(0, mCM.mRequested.size());
+
+ // [2] WiFi connects but not validated/promoted to default -> mobile selected.
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
+ wifiAgent.fakeConnect();
+ mLooper.dispatchAll();
+ assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(0, mCM.mRequested.size());
+
+ // [3] WiFi validates and is promoted to the default network -> WiFi selected.
+ mCM.makeDefaultNetwork(wifiAgent);
+ mLooper.dispatchAll();
+ assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(0, mCM.mRequested.size());
+
+ // [4] DUN required, no other changes -> WiFi still selected
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(1, mCM.mRequested.size());
+ assertTrue(isDunRequested());
+
+ // [5] WiFi no longer validated, mobile becomes default, DUN required -> null selected.
+ mCM.makeDefaultNetwork(cellAgent);
+ mLooper.dispatchAll();
+ assertEquals(null, mUNM.getCurrentPreferredUpstream());
+ assertEquals(1, mCM.mRequested.size());
+ assertTrue(isDunRequested());
+
+ // [6] DUN network arrives -> DUN selected
+ final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
+ dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
+ dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
+ dunAgent.fakeConnect();
+ mLooper.dispatchAll();
+ assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(1, mCM.mRequested.size());
+
+ // [7] Mobile is not permitted -> null selected
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
+ assertEquals(null, mUNM.getCurrentPreferredUpstream());
+ assertEquals(1, mCM.mRequested.size());
+
+ // [7] Mobile is permitted again -> DUN selected
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(1, mCM.mRequested.size());
+
+ // [8] DUN no longer required -> request is withdrawn
+ mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
+ assertEquals(0, mCM.mRequested.size());
+ assertFalse(isDunRequested());
+ }
+
+ @Test
+ public void testLocalPrefixes() throws Exception {
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+ mUNM.startObserveAllNetworks();
+
+ // [0] Test minimum set of local prefixes.
+ Set<IpPrefix> local = mUNM.getLocalPrefixes();
+ assertTrue(local.isEmpty());
+
+ final Set<String> alreadySeen = new HashSet<>();
+
+ // [1] Pretend Wi-Fi connects.
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
+ final LinkProperties wifiLp = wifiAgent.linkProperties;
+ wifiLp.setInterfaceName("wlan0");
+ final String[] wifi_addrs = {
+ "fe80::827a:bfff:fe6f:374d", "100.112.103.18",
+ "2001:db8:4:fd00:827a:bfff:fe6f:374d",
+ "2001:db8:4:fd00:6dea:325a:fdae:4ef4",
+ "fd6a:a640:60bf:e985::123", // ULA address for good measure.
+ };
+ for (String addrStr : wifi_addrs) {
+ final String cidr = addrStr.contains(":") ? "/64" : "/20";
+ wifiLp.addLinkAddress(new LinkAddress(addrStr + cidr));
+ }
+ wifiAgent.fakeConnect();
+ wifiAgent.sendLinkProperties();
+ mLooper.dispatchAll();
+
+ local = mUNM.getLocalPrefixes();
+ assertPrefixSet(local, INCLUDES, alreadySeen);
+ final String[] wifiLinkPrefixes = {
+ // Link-local prefixes are excluded and dealt with elsewhere.
+ "100.112.96.0/20", "2001:db8:4:fd00::/64", "fd6a:a640:60bf:e985::/64",
+ };
+ assertPrefixSet(local, INCLUDES, wifiLinkPrefixes);
+ Collections.addAll(alreadySeen, wifiLinkPrefixes);
+ assertEquals(alreadySeen.size(), local.size());
+
+ // [2] Pretend mobile connects.
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
+ final LinkProperties cellLp = cellAgent.linkProperties;
+ cellLp.setInterfaceName("rmnet_data0");
+ final String[] cell_addrs = {
+ "10.102.211.48", "2001:db8:0:1:b50e:70d9:10c9:433d",
+ };
+ for (String addrStr : cell_addrs) {
+ final String cidr = addrStr.contains(":") ? "/64" : "/27";
+ cellLp.addLinkAddress(new LinkAddress(addrStr + cidr));
+ }
+ cellAgent.fakeConnect();
+ cellAgent.sendLinkProperties();
+ mLooper.dispatchAll();
+
+ local = mUNM.getLocalPrefixes();
+ assertPrefixSet(local, INCLUDES, alreadySeen);
+ final String[] cellLinkPrefixes = { "10.102.211.32/27", "2001:db8:0:1::/64" };
+ assertPrefixSet(local, INCLUDES, cellLinkPrefixes);
+ Collections.addAll(alreadySeen, cellLinkPrefixes);
+ assertEquals(alreadySeen.size(), local.size());
+
+ // [3] Pretend DUN connects.
+ final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES);
+ final LinkProperties dunLp = dunAgent.linkProperties;
+ dunLp.setInterfaceName("rmnet_data1");
+ final String[] dun_addrs = {
+ "192.0.2.48", "2001:db8:1:2:b50e:70d9:10c9:433d",
+ };
+ for (String addrStr : dun_addrs) {
+ final String cidr = addrStr.contains(":") ? "/64" : "/27";
+ dunLp.addLinkAddress(new LinkAddress(addrStr + cidr));
+ }
+ dunAgent.fakeConnect();
+ dunAgent.sendLinkProperties();
+ mLooper.dispatchAll();
+
+ local = mUNM.getLocalPrefixes();
+ assertPrefixSet(local, INCLUDES, alreadySeen);
+ final String[] dunLinkPrefixes = { "192.0.2.32/27", "2001:db8:1:2::/64" };
+ assertPrefixSet(local, INCLUDES, dunLinkPrefixes);
+ Collections.addAll(alreadySeen, dunLinkPrefixes);
+ assertEquals(alreadySeen.size(), local.size());
+
+ // [4] Pretend Wi-Fi disconnected. It's addresses/prefixes should no
+ // longer be included (should be properly removed).
+ wifiAgent.fakeDisconnect();
+ mLooper.dispatchAll();
+ local = mUNM.getLocalPrefixes();
+ assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes);
+ assertPrefixSet(local, INCLUDES, cellLinkPrefixes);
+ assertPrefixSet(local, INCLUDES, dunLinkPrefixes);
+
+ // [5] Pretend mobile disconnected.
+ cellAgent.fakeDisconnect();
+ mLooper.dispatchAll();
+ local = mUNM.getLocalPrefixes();
+ assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes);
+ assertPrefixSet(local, EXCLUDES, cellLinkPrefixes);
+ assertPrefixSet(local, INCLUDES, dunLinkPrefixes);
+
+ // [6] Pretend DUN disconnected.
+ dunAgent.fakeDisconnect();
+ mLooper.dispatchAll();
+ local = mUNM.getLocalPrefixes();
+ assertTrue(local.isEmpty());
+ }
+
+ @Test
+ public void testSelectMobileWhenMobileIsNotDefault() {
+ final Collection<Integer> preferredTypes = new ArrayList<>();
+ // Mobile has higher pirority than wifi.
+ preferredTypes.add(TYPE_MOBILE_HIPRI);
+ preferredTypes.add(TYPE_WIFI);
+ mUNM.startTrackDefaultNetwork(mEntitleMgr);
+ mUNM.startObserveAllNetworks();
+ // Setup wifi and make wifi as default network.
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
+ wifiAgent.fakeConnect();
+ mCM.makeDefaultNetwork(wifiAgent);
+ // Setup mobile network.
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
+ cellAgent.fakeConnect();
+ mLooper.dispatchAll();
+
+ assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
+ mUNM.selectPreferredUpstreamType(preferredTypes));
+ verify(mEntitleMgr, times(1)).maybeRunProvisioning();
+ }
+
+ private void assertSatisfiesLegacyType(int legacyType, UpstreamNetworkState ns) {
+ if (legacyType == TYPE_NONE) {
+ assertTrue(ns == null);
+ return;
+ }
+
+ final NetworkCapabilities nc =
+ UpstreamNetworkMonitor.networkCapabilitiesForType(legacyType);
+ assertTrue(nc.satisfiedByNetworkCapabilities(ns.networkCapabilities));
+ }
+
+ private void assertUpstreamTypeRequested(int upstreamType) throws Exception {
+ assertEquals(1, mCM.mRequested.size());
+ assertEquals(1, mCM.mLegacyTypeMap.size());
+ assertEquals(Integer.valueOf(upstreamType),
+ mCM.mLegacyTypeMap.values().iterator().next());
+ }
+
+ private boolean isDunRequested() {
+ for (NetworkRequestInfo nri : mCM.mRequested.values()) {
+ if (nri.request.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static class TestStateMachine extends StateMachine {
+ public final ArrayList<Message> messages = new ArrayList<>();
+ private final State mLoggingState = new LoggingState();
+
+ class LoggingState extends State {
+ @Override public void enter() {
+ messages.clear();
+ }
+
+ @Override public void exit() {
+ messages.clear();
+ }
+
+ @Override public boolean processMessage(Message msg) {
+ messages.add(msg);
+ return true;
+ }
+ }
+
+ public TestStateMachine(Looper looper) {
+ super("UpstreamNetworkMonitor.TestStateMachine", looper);
+ addState(mLoggingState);
+ setInitialState(mLoggingState);
+ super.start();
+ }
+ }
+
+ static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
+ final Set<String> expectedSet = new HashSet<>();
+ Collections.addAll(expectedSet, expected);
+ assertPrefixSet(prefixes, expectation, expectedSet);
+ }
+
+ static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, Set<String> expected) {
+ for (String expectedPrefix : expected) {
+ final String errStr = expectation ? "did not find" : "found";
+ assertEquals(
+ String.format("Failed expectation: %s prefix: %s", errStr, expectedPrefix),
+ expectation, prefixes.contains(new IpPrefix(expectedPrefix)));
+ }
+ }
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index 6eb8348..ee71e15 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -16,11 +16,7 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
filegroup {
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
index df37ae8..099202f 100644
--- a/framework/lint-baseline.xml
+++ b/framework/lint-baseline.xml
@@ -7,7 +7,7 @@
errorLine1=" ParseException pe = new ParseException(e.reason, e.getCause());"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/framework/src/android/net/DnsResolver.java"
+ file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java"
line="301"
column="37"/>
</issue>
@@ -18,7 +18,7 @@
errorLine1=" protected class ActiveDataSubscriptionIdListener extends TelephonyCallback"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+ file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
line="96"
column="62"/>
</issue>
@@ -29,7 +29,7 @@
errorLine1=" implements TelephonyCallback.ActiveDataSubscriptionIdListener {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+ file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
line="97"
column="24"/>
</issue>
@@ -40,7 +40,7 @@
errorLine1=" ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback("
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+ file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
line="126"
column="54"/>
</issue>
diff --git a/service/Android.bp b/service/Android.bp
index 7265426..28bcdcb 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -16,11 +16,7 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
cc_library_shared {
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 95c169c..119b64f 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -7,7 +7,7 @@
errorLine1=" if (tm.isDataCapable()) {"
errorLine2=" ~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
line="787"
column="20"/>
</issue>
@@ -18,7 +18,7 @@
errorLine1=" mUserAllContext.sendStickyBroadcast(intent, options);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
line="2681"
column="33"/>
</issue>
@@ -29,7 +29,7 @@
errorLine1=" final int callingVersion = pm.getTargetSdkVersion(callingPackageName);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/packages/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
line="5851"
column="43"/>
</issue>
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 7331453..e8963b9 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -18,11 +18,7 @@
// They must be fast and stable, and exercise public or test APIs.
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
new file mode 100644
index 0000000..4264345
--- /dev/null
+++ b/tests/cts/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 31808
+set noparent
+lorenzo@google.com
+satk@google.com
\ No newline at end of file
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
new file mode 100644
index 0000000..3185f7e
--- /dev/null
+++ b/tests/cts/hostside/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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"],
+}
+
+java_test_host {
+ name: "CtsHostsideNetworkTests",
+ defaults: ["cts_defaults"],
+ // Only compile source java files in this apk.
+ srcs: ["src/**/*.java"],
+ libs: [
+ "cts-tradefed",
+ "tradefed",
+ ],
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+}
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
new file mode 100644
index 0000000..7a73313
--- /dev/null
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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 net 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="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.net.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.net.hostside.app2" />
+ <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.net.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>
+
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="CtsHostsideNetworkTests.jar" />
+ <option name="runtime-hint" value="3m56s" />
+ </test>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/sdcard/CtsHostsideNetworkTests" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+</configuration>
diff --git a/tests/cts/hostside/OWNERS b/tests/cts/hostside/OWNERS
new file mode 100644
index 0000000..20bc55e
--- /dev/null
+++ b/tests/cts/hostside/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 61373
+# Inherits parent owners
+sudheersai@google.com
+jchalard@google.com
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
new file mode 100644
index 0000000..fcec483
--- /dev/null
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit-large": [
+ {
+ "name": "CtsHostsideNetworkTests",
+ "options": [
+ {
+ "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
new file mode 100644
index 0000000..2751f6f
--- /dev/null
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -0,0 +1,28 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_helper_library {
+ name: "CtsHostsideNetworkTestsAidl",
+ sdk_version: "current",
+ srcs: [
+ "com/android/cts/net/hostside/IMyService.aidl",
+ "com/android/cts/net/hostside/INetworkCallback.aidl",
+ "com/android/cts/net/hostside/INetworkStateObserver.aidl",
+ "com/android/cts/net/hostside/IRemoteSocketFactory.aidl",
+ ],
+}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
new file mode 100644
index 0000000..28437c2
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.net.hostside;
+
+import android.app.job.JobInfo;
+
+import com.android.cts.net.hostside.INetworkCallback;
+
+interface IMyService {
+ void registerBroadcastReceiver();
+ int getCounters(String receiverName, String action);
+ String checkNetworkStatus();
+ String getRestrictBackgroundStatus();
+ void sendNotification(int notificationId, String notificationType);
+ void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
+ void unregisterNetworkCallback();
+ void scheduleJob(in JobInfo jobInfo);
+}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
new file mode 100644
index 0000000..2048bab
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.net.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/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
new file mode 100644
index 0000000..19198c5
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.net.hostside;
+
+interface INetworkStateObserver {
+ void onNetworkStateChecked(int resultCode, String resultData);
+
+ 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/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
new file mode 100644
index 0000000..68176ad
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.net.hostside;
+
+import android.os.ParcelFileDescriptor;
+
+interface IRemoteSocketFactory {
+ ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs);
+ String getPackageName();
+ int getUid();
+}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
new file mode 100644
index 0000000..5b2369c
--- /dev/null
+++ b/tests/cts/hostside/app/Android.bp
@@ -0,0 +1,48 @@
+//
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ ],
+ platform_apis: true,
+ static_libs: [
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "cts-net-utils",
+ "ctstestrunner-axt",
+ "ub-uiautomator",
+ "CtsHostsideNetworkTestsAidl",
+ "modules-utils-build",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ srcs: ["src/**/*.java"],
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+}
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
new file mode 100644
index 0000000..e5bae5f
--- /dev/null
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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.net.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.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"/>
+
+ <application android:requestLegacyExternalStorage="true">
+ <uses-library android:name="android.test.runner"/>
+ <activity android:name=".MyActivity"/>
+ <service android:name=".MyVpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.net.VpnService"/>
+ </intent-filter>
+ </service>
+ <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.net.hostside"/>
+
+</manifest>
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
new file mode 100644
index 0000000..d9ff539
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
@@ -0,0 +1,201 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.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/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
new file mode 100644
index 0000000..04d054d
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
@@ -0,0 +1,111 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.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);
+ assertForegroundNetworkAccess();
+
+ // Although it should not have access while the screen is off.
+ turnScreenOff();
+ assertBackgroundNetworkAccess(false);
+ turnScreenOn();
+ assertForegroundNetworkAccess();
+
+ // 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);
+ assertForegroundNetworkAccess();
+ 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/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
new file mode 100644
index 0000000..e0ce4ea
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
@@ -0,0 +1,141 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.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);
+ assertForegroundNetworkAccess();
+ stopForegroundService();
+ assertBackgroundState();
+ 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();
+ assertBackgroundState();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
new file mode 100644
index 0000000..a850e3b
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.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();
+ 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/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
new file mode 100644
index 0000000..f9454ad
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -0,0 +1,974 @@
+/*
+ * 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.net.hostside;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
+import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
+import static android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
+
+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.app.ActivityManager;
+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.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.SystemClock;
+import android.provider.DeviceConfig;
+import android.service.notification.NotificationListenerService;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
+
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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.net.hostside";
+ protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+
+ 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.net.hostside.app2.action.FINISH_ACTIVITY";
+ private static final String ACTION_FINISH_JOB =
+ "com.android.cts.net.hostside.app2.action.FINISH_JOB";
+
+ private static final String ACTION_RECEIVER_READY =
+ "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
+ static final String ACTION_SHOW_TOAST =
+ "com.android.cts.net.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";
+
+ // TODO: Update BatteryManager.BATTERY_PLUGGED_ANY as @TestApi
+ public static final int BATTERY_PLUGGED_ANY =
+ BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS;
+
+ 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 int PROCESS_STATE_FOREGROUND_SERVICE;
+
+ private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
+ private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
+
+ 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 = 6_000;
+ private static final int JOB_NETWORK_STATE_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);
+
+ private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
+
+ protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 5_000; // 5 sec
+
+ protected Context mContext;
+ protected Instrumentation mInstrumentation;
+ protected ConnectivityManager mCm;
+ protected int mUid;
+ private int mMyUid;
+ private MyServiceClient mServiceClient;
+ private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
+
+ @Rule
+ public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
+ .around(new MeterednessConfigurationRule());
+
+ protected void setUp() throws Exception {
+ // TODO: Annotate these constants with @TestApi instead of obtaining them using reflection
+ PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
+ .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
+ mInstrumentation = getInstrumentation();
+ mContext = getContext();
+ mCm = getConnectivityManager();
+ mDeviceIdleDeviceConfigStateHelper =
+ new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_DEVICE_IDLE);
+ mUid = getUid(TEST_APP2_PKG);
+ mMyUid = getUid(mContext.getPackageName());
+ mServiceClient = new MyServiceClient(mContext);
+ mServiceClient.bind();
+ executeShellCommand("cmd netpolicy start-watching " + mUid);
+ setAppIdle(false);
+
+ 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();
+ }
+
+ 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");
+ 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 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");
+ 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)));
+ }
+
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertBackgroundState();
+ assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
+ }
+
+ protected void assertForegroundNetworkAccess() throws Exception {
+ assertForegroundNetworkAccess(true);
+ }
+
+ protected void assertForegroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertForegroundState();
+ // We verified that app is in foreground state but if the screen turns-off while
+ // verifying for network access, the app will go into background state (in case app's
+ // foreground status was due to top activity). So, turn the screen on when verifying
+ // network connectivity.
+ assertNetworkAccess(expectAllowed /* expectAvailable */, true /* needScreenOn */);
+ }
+
+ protected void 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();
+ }
+
+ protected final void assertBackgroundState() throws Exception {
+ final int maxTries = 30;
+ ProcessState state = null;
+ for (int i = 1; i <= maxTries; i++) {
+ state = getProcessStateByUid(mUid);
+ Log.v(TAG, "assertBackgroundState(): status for app2 (" + mUid + ") on attempt #" + i
+ + ": " + state);
+ if (isBackground(state.state)) {
+ return;
+ }
+ Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
+ + "; sleeping 1s before trying again");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("App2 is not on background state after " + maxTries + " attempts: " + state );
+ }
+
+ protected final void assertForegroundState() throws Exception {
+ final int maxTries = 30;
+ ProcessState state = null;
+ for (int i = 1; i <= maxTries; i++) {
+ state = getProcessStateByUid(mUid);
+ Log.v(TAG, "assertForegroundState(): status for app2 (" + mUid + ") on attempt #" + i
+ + ": " + state);
+ if (!isBackground(state.state)) {
+ return;
+ }
+ Log.d(TAG, "App not on foreground state on attempt #" + i
+ + "; sleeping 1s before trying again");
+ turnScreenOn();
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("App2 is not on foreground state after " + maxTries + " attempts: " + state );
+ }
+
+ protected final void assertForegroundServiceState() throws Exception {
+ final int maxTries = 30;
+ ProcessState state = null;
+ for (int i = 1; i <= maxTries; i++) {
+ state = getProcessStateByUid(mUid);
+ Log.v(TAG, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #"
+ + i + ": " + state);
+ if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) {
+ return;
+ }
+ Log.d(TAG, "App not on foreground service state on attempt #" + i
+ + "; sleeping 1s before trying again");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("App2 is not on foreground service state after " + maxTries + " attempts: " + state );
+ }
+
+ /**
+ * Returns whether an app state should be considered "background" for restriction purposes.
+ */
+ protected boolean isBackground(int state) {
+ return state > PROCESS_STATE_FOREGROUND_SERVICE;
+ }
+
+ /**
+ * Asserts whether the active network is available or not.
+ */
+ private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
+ throws Exception {
+ final int maxTries = 5;
+ String error = null;
+ int timeoutMs = 500;
+
+ for (int i = 1; i <= maxTries; i++) {
+ error = checkNetworkAccess(expectAvailable);
+
+ 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 expectAvailable=" + expectAvailable + " after " + maxTries
+ + " attempts.\nLast error: " + error);
+ }
+
+ /**
+ * Checks whether the network is available as expected.
+ *
+ * @return error message with the mismatch (or empty if assertion passed).
+ */
+ private String checkNetworkAccess(boolean expectAvailable) throws Exception {
+ final String resultData = mServiceClient.checkNetworkStatus();
+ return checkForAvailabilityInResultData(resultData, expectAvailable);
+ }
+
+ private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
+ if (resultData == null) {
+ assertNotNull("Network status from app2 is null", resultData);
+ }
+ // Network status format is described on MyBroadcastReceiver.checkNetworkStatus()
+ final String[] parts = resultData.split(NETWORK_STATUS_SEPARATOR);
+ assertEquals("Wrong network status: " + resultData, 5, parts.length);
+ final State state = parts[0].equals("null") ? null : State.valueOf(parts[0]);
+ final DetailedState detailedState = parts[1].equals("null")
+ ? null : DetailedState.valueOf(parts[1]);
+ final boolean connected = Boolean.valueOf(parts[2]);
+ final String connectionCheckDetails = parts[3];
+ final String networkInfo = parts[4];
+
+ 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));
+ }
+
+ 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");
+ 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");
+ 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 {
+ executeSilentShellCommand("input keyevent KEYCODE_SLEEP");
+ }
+
+ protected void turnScreenOn() throws Exception {
+ executeSilentShellCommand("input keyevent KEYCODE_WAKEUP");
+ 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();
+ executeSilentShellCommand("cmd power set-mode 1");
+ } else {
+ executeSilentShellCommand("cmd power set-mode 0");
+ turnBatteryOff();
+ }
+ }
+
+ 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 setAppIdle(boolean enabled) throws Exception {
+ if (!isAppStandbySupported()) {
+ return;
+ }
+ Log.i(TAG, "Setting app idle to " + enabled);
+ executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
+ assertAppIdle(enabled);
+ }
+
+ protected void setAppIdleNoAssert(boolean enabled) throws Exception {
+ if (!isAppStandbySupported()) {
+ return;
+ }
+ Log.i(TAG, "Setting app idle to " + enabled);
+ executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
+ }
+
+ protected void assertAppIdle(boolean enabled) throws Exception {
+ try {
+ assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
+ } 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");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("app2 receiver is not ready");
+ }
+
+ 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 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();
+ return;
+ } else if (type == TYPE_COMPONENT_ACTIVTIY) {
+ turnScreenOn();
+ // Wait for screen-on state to propagate through the system.
+ SystemClock.sleep(2000);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Intent launchIntent = getIntentForComponent(type);
+ final Bundle extras = new Bundle();
+ final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+ extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
+ extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+ launchIntent.putExtras(extras);
+ mContext.startActivity(launchIntent);
+ if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ final int resultCode = result.get(0).first;
+ final String resultData = result.get(0).second;
+ if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
+ final String error = checkForAvailabilityInResultData(
+ resultData, expectAvailable);
+ 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, resultData);
+ // App didn't come to foreground when the activity is started, so try again.
+ assertForegroundNetworkAccess();
+ } else {
+ fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+ }
+ } 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 ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+ final CountDownLatch latch = new CountDownLatch(1);
+ extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
+ extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+ final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
+ .setExpedited(true)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+ .setTransientExtras(extras)
+ .build();
+ 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(0).first;
+ final String resultData = result.get(0).second;
+ if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
+ final String error = checkForAvailabilityInResultData(
+ resultData, expectAvailable);
+ if (error != null) {
+ fail("Network is not available for expedited job in app2 (" + mUid + "): "
+ + error);
+ }
+ } else {
+ fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+ }
+ } else {
+ fail("Timed out waiting for network availability status from app2's expedited job ("
+ + mUid + ")");
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown type: " + type);
+ }
+ }
+
+ 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);
+ } 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 ArrayList<Pair<Integer, String>> result) {
+ return new INetworkStateObserver.Stub() {
+ @Override
+ public void onNetworkStateChecked(int resultCode, String resultData) {
+ result.add(Pair.create(resultCode, resultData));
+ 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();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java
new file mode 100644
index 0000000..f1858d6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java
new file mode 100644
index 0000000..e737a6d
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java
new file mode 100644
index 0000000..c78ca2e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java
new file mode 100644
index 0000000..fb52a54
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java
@@ -0,0 +1,24 @@
+/*
+ * 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.net.hostside;
+
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
new file mode 100644
index 0000000..604a0b6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.net.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.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NO_DATA_SAVER_MODE;
+
+import static org.junit.Assert.fail;
+
+import com.android.compatibility.common.util.CddTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import androidx.test.filters.LargeTest;
+
+@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);
+ assertForegroundNetworkAccess();
+
+ // Although it should not have access while the screen is off.
+ turnScreenOff();
+ assertBackgroundNetworkAccess(false);
+ turnScreenOn();
+ assertForegroundNetworkAccess();
+
+ // 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);
+ assertForegroundNetworkAccess();
+ 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/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java
new file mode 100644
index 0000000..4306c99
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java
new file mode 100644
index 0000000..1e89f15
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
new file mode 100644
index 0000000..66cb935
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
@@ -0,0 +1,102 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
+import static com.android.cts.net.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 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(),
+ "CtsHostsideNetworkTests");
+
+ @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 File dumpFile = new File(mDumpDir, "dump-" + getShortenedTestName(description));
+ 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",
+ }) {
+ 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);
+ }
+ }
+
+ 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/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java
new file mode 100644
index 0000000..3809534
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
new file mode 100644
index 0000000..6596269
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
new file mode 100644
index 0000000..5c99c67
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
@@ -0,0 +1,59 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.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/app/src/com/android/cts/net/hostside/MixedModesTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java
new file mode 100644
index 0000000..c9edda6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.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/app/src/com/android/cts/net/hostside/MyActivity.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
new file mode 100644
index 0000000..55eec11
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class MyActivity extends Activity {
+ private final LinkedBlockingQueue<Integer> mResult = new LinkedBlockingQueue<>(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+
+ // Dismiss the keyguard so that the tests can click on the VPN confirmation dialog.
+ // FLAG_DISMISS_KEYGUARD is not sufficient to do this because as soon as the dialog appears,
+ // this activity goes into the background and the keyguard reappears.
+ getSystemService(KeyguardManager.class).requestDismissKeyguard(this, null /* callback */);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (mResult.offer(resultCode) == false) {
+ throw new RuntimeException("Queue is full! This should never happen");
+ }
+ }
+
+ public Integer getResult(int timeoutMs) throws InterruptedException {
+ return mResult.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
new file mode 100644
index 0000000..0132536
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
@@ -0,0 +1,123 @@
+/*
+ * 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.net.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/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
new file mode 100644
index 0000000..8b70f9b
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -0,0 +1,113 @@
+/*
+ * 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.net.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 = 5000;
+ 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 IMyService mService;
+
+ public MyServiceClient(Context context) {
+ mContext = context;
+ }
+
+ public void bind() {
+ if (mService != null) {
+ throw new IllegalStateException("Already bound");
+ }
+
+ final ConditionVariable cv = new ConditionVariable();
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IMyService.Stub.asInterface(service);
+ cv.open();
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ 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
+ | Context.BIND_NOT_FOREGROUND);
+ cv.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ public void unbind() {
+ if (mService != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ }
+
+ public void registerBroadcastReceiver() throws RemoteException {
+ mService.registerBroadcastReceiver();
+ }
+
+ public int getCounters(String receiverName, String action) throws RemoteException {
+ return mService.getCounters(receiverName, action);
+ }
+
+ public String checkNetworkStatus() throws RemoteException {
+ return mService.checkNetworkStatus();
+ }
+
+ public String getRestrictBackgroundStatus() throws RemoteException {
+ return mService.getRestrictBackgroundStatus();
+ }
+
+ public void sendNotification(int notificationId, String notificationType)
+ throws RemoteException {
+ mService.sendNotification(notificationId, notificationType);
+ }
+
+ public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
+ throws RemoteException {
+ mService.registerNetworkCallback(request, cb);
+ }
+
+ public void unregisterNetworkCallback() throws RemoteException {
+ mService.unregisterNetworkCallback();
+ }
+
+ public void scheduleJob(JobInfo jobInfo) throws RemoteException {
+ mService.scheduleJob(jobInfo);
+ }
+}
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
new file mode 100644
index 0000000..7d3d4fc
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import android.content.Intent;
+import android.net.Network;
+import android.net.ProxyInfo;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+
+public class MyVpnService extends VpnService {
+
+ private static String TAG = "MyVpnService";
+ private static int MTU = 1799;
+
+ public static final String ACTION_ESTABLISHED = "com.android.cts.net.hostside.ESTABNLISHED";
+ public static final String EXTRA_ALWAYS_ON = "is-always-on";
+ public static final String EXTRA_LOCKDOWN_ENABLED = "is-lockdown-enabled";
+
+ private ParcelFileDescriptor mFd = null;
+ private PacketReflector mPacketReflector = null;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ String packageName = getPackageName();
+ String cmd = intent.getStringExtra(packageName + ".cmd");
+ if ("disconnect".equals(cmd)) {
+ stop();
+ } else if ("connect".equals(cmd)) {
+ start(packageName, intent);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void start(String packageName, Intent intent) {
+ Builder builder = new Builder();
+
+ String addresses = intent.getStringExtra(packageName + ".addresses");
+ if (addresses != null) {
+ String[] addressArray = addresses.split(",");
+ for (int i = 0; i < addressArray.length; i++) {
+ String[] prefixAndMask = addressArray[i].split("/");
+ try {
+ InetAddress address = InetAddress.getByName(prefixAndMask[0]);
+ int prefixLength = Integer.parseInt(prefixAndMask[1]);
+ builder.addAddress(address, prefixLength);
+ } catch (UnknownHostException|NumberFormatException|
+ ArrayIndexOutOfBoundsException e) {
+ continue;
+ }
+ }
+ }
+
+ String routes = intent.getStringExtra(packageName + ".routes");
+ if (routes != null) {
+ String[] routeArray = routes.split(",");
+ for (int i = 0; i < routeArray.length; i++) {
+ String[] prefixAndMask = routeArray[i].split("/");
+ try {
+ InetAddress address = InetAddress.getByName(prefixAndMask[0]);
+ int prefixLength = Integer.parseInt(prefixAndMask[1]);
+ builder.addRoute(address, prefixLength);
+ } catch (UnknownHostException|NumberFormatException|
+ ArrayIndexOutOfBoundsException e) {
+ continue;
+ }
+ }
+ }
+
+ String allowed = intent.getStringExtra(packageName + ".allowedapplications");
+ if (allowed != null) {
+ String[] packageArray = allowed.split(",");
+ for (int i = 0; i < packageArray.length; i++) {
+ String allowedPackage = packageArray[i];
+ if (!TextUtils.isEmpty(allowedPackage)) {
+ try {
+ builder.addAllowedApplication(allowedPackage);
+ } catch(NameNotFoundException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ String disallowed = intent.getStringExtra(packageName + ".disallowedapplications");
+ if (disallowed != null) {
+ String[] packageArray = disallowed.split(",");
+ for (int i = 0; i < packageArray.length; i++) {
+ String disallowedPackage = packageArray[i];
+ if (!TextUtils.isEmpty(disallowedPackage)) {
+ try {
+ builder.addDisallowedApplication(disallowedPackage);
+ } catch(NameNotFoundException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ ArrayList<Network> underlyingNetworks =
+ intent.getParcelableArrayListExtra(packageName + ".underlyingNetworks");
+ if (underlyingNetworks == null) {
+ // VPN tracks default network
+ builder.setUnderlyingNetworks(null);
+ } else {
+ builder.setUnderlyingNetworks(underlyingNetworks.toArray(new Network[0]));
+ }
+
+ boolean isAlwaysMetered = intent.getBooleanExtra(packageName + ".isAlwaysMetered", false);
+ builder.setMetered(isAlwaysMetered);
+
+ ProxyInfo vpnProxy = intent.getParcelableExtra(packageName + ".httpProxy");
+ builder.setHttpProxy(vpnProxy);
+ builder.setMtu(MTU);
+ builder.setBlocking(true);
+ builder.setSession("MyVpnService");
+
+ Log.i(TAG, "Establishing VPN,"
+ + " addresses=" + addresses
+ + " routes=" + routes
+ + " allowedApplications=" + allowed
+ + " disallowedApplications=" + disallowed);
+
+ mFd = builder.establish();
+ Log.i(TAG, "Established, fd=" + (mFd == null ? "null" : mFd.getFd()));
+
+ broadcastEstablished();
+
+ mPacketReflector = new PacketReflector(mFd.getFileDescriptor(), MTU);
+ mPacketReflector.start();
+ }
+
+ private void broadcastEstablished() {
+ final Intent bcIntent = new Intent(ACTION_ESTABLISHED);
+ bcIntent.putExtra(EXTRA_ALWAYS_ON, isAlwaysOn());
+ bcIntent.putExtra(EXTRA_LOCKDOWN_ENABLED, isLockdownEnabled());
+ sendBroadcast(bcIntent);
+ }
+
+ private void stop() {
+ if (mPacketReflector != null) {
+ mPacketReflector.interrupt();
+ mPacketReflector = null;
+ }
+ try {
+ if (mFd != null) {
+ Log.i(TAG, "Closing filedescriptor");
+ mFd.close();
+ }
+ } catch(IOException e) {
+ } finally {
+ mFd = null;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ stop();
+ super.onDestroy();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
new file mode 100644
index 0000000..0715e32
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.net.hostside;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.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.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+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();
+ @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 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);
+
+ // 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.expectBlockedStatusCallback(mNetwork, false);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ setRestrictBackground(false);
+ setBatterySaverMode(false);
+ unregisterNetworkCallback();
+ }
+
+ @RequiredProperties({DATA_SAVER_MODE})
+ @Test
+ public void testOnBlockedStatusChanged_dataSaver() throws Exception {
+ try {
+ // Enable restrict background
+ setRestrictBackground(true);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+
+ // Add to whitelist
+ addRestrictBackgroundWhitelist(mUid);
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+
+ // Remove from whitelist
+ removeRestrictBackgroundWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+ 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);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+
+ // Disable restrict background, should not trigger callback
+ setRestrictBackground(false);
+ assertBackgroundNetworkAccess(true);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
+ @RequiredProperties({BATTERY_SAVER_MODE})
+ @Test
+ public void testOnBlockedStatusChanged_powerSaver() throws Exception {
+ try {
+ // Enable Power Saver
+ setBatterySaverMode(true);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+
+ // Disable Power Saver
+ setBatterySaverMode(false);
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ } 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);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+
+ // Disable Power Saver
+ setBatterySaverMode(false);
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
+ // TODO: 1. test against VPN lockdown.
+ // 2. test against multiple networks.
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java
new file mode 100644
index 0000000..f340907
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java
@@ -0,0 +1,44 @@
+/*
+ * 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.net.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/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
new file mode 100644
index 0000000..7da1a21
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -0,0 +1,418 @@
+/*
+ * 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.net.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.runShellCommand;
+import static com.android.cts.net.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.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+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 com.android.compatibility.common.util.AppStandbyUtils;
+import com.android.compatibility.common.util.BatteryUtils;
+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 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;
+ }
+
+ /**
+ * 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 (mDataSaverSupported == null) {
+ 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);
+ }
+
+ private 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 String executeShellCommand(String command) {
+ final String result = runShellCommand(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 Context getContext() {
+ return getInstrumentation().getContext();
+ }
+
+ public static Instrumentation getInstrumentation() {
+ return InstrumentationRegistry.getInstrumentation();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java
new file mode 100644
index 0000000..124c2c3
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static android.system.OsConstants.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
+import static android.system.OsConstants.ICMP_ECHO;
+import static android.system.OsConstants.ICMP_ECHOREPLY;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+public class PacketReflector extends Thread {
+
+ private static int IPV4_HEADER_LENGTH = 20;
+ private static int IPV6_HEADER_LENGTH = 40;
+
+ private static int IPV4_ADDR_OFFSET = 12;
+ private static int IPV6_ADDR_OFFSET = 8;
+ private static int IPV4_ADDR_LENGTH = 4;
+ private static int IPV6_ADDR_LENGTH = 16;
+
+ private static int IPV4_PROTO_OFFSET = 9;
+ private static int IPV6_PROTO_OFFSET = 6;
+
+ private static final byte IPPROTO_ICMP = 1;
+ private static final byte IPPROTO_TCP = 6;
+ private static final byte IPPROTO_UDP = 17;
+ private static final byte IPPROTO_ICMPV6 = 58;
+
+ private static int ICMP_HEADER_LENGTH = 8;
+ private static int TCP_HEADER_LENGTH = 20;
+ private static int UDP_HEADER_LENGTH = 8;
+
+ private static final byte ICMP_ECHO = 8;
+ private static final byte ICMP_ECHOREPLY = 0;
+
+ private static String TAG = "PacketReflector";
+
+ private FileDescriptor mFd;
+ private byte[] mBuf;
+
+ public PacketReflector(FileDescriptor fd, int mtu) {
+ super("PacketReflector");
+ mFd = fd;
+ mBuf = new byte[mtu];
+ }
+
+ private static void swapBytes(byte[] buf, int pos1, int pos2, int len) {
+ for (int i = 0; i < len; i++) {
+ byte b = buf[pos1 + i];
+ buf[pos1 + i] = buf[pos2 + i];
+ buf[pos2 + i] = b;
+ }
+ }
+
+ private static void swapAddresses(byte[] buf, int version) {
+ int addrPos, addrLen;
+ switch(version) {
+ case 4:
+ addrPos = IPV4_ADDR_OFFSET;
+ addrLen = IPV4_ADDR_LENGTH;
+ break;
+ case 6:
+ addrPos = IPV6_ADDR_OFFSET;
+ addrLen = IPV6_ADDR_LENGTH;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
+ }
+
+ // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+ // This is used by the test to "connect to itself" through the VPN.
+ private void processTcpPacket(byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + TCP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+ // This is used by the test to check that the bytes it sends are echoed back.
+ private void processUdpPacket(byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + UDP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Swap dst and src ports.
+ int portOffset = hdrLen;
+ swapBytes(buf, portOffset, portOffset + 2, 2);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ private void processIcmpPacket(byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + ICMP_HEADER_LENGTH) {
+ return;
+ }
+
+ byte type = buf[hdrLen];
+ if (!(version == 4 && type == ICMP_ECHO) &&
+ !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) {
+ return;
+ }
+
+ // Save the ping packet we received.
+ byte[] request = buf.clone();
+
+ // Swap src and dst IP addresses, and send the packet back.
+ // This effectively pings the device to see if it replies.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+
+ // The device should have replied, and buf should now contain a ping response.
+ int received = readPacket(buf);
+ if (received != len) {
+ Log.i(TAG, "Reflecting ping did not result in ping response: " +
+ "read=" + received + " expected=" + len);
+ return;
+ }
+
+ byte replyType = buf[hdrLen];
+ if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY)
+ || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) {
+ Log.i(TAG, "Received unexpected ICMP reply: original " + type
+ + ", reply " + replyType);
+ return;
+ }
+
+ // Compare the response we got with the original packet.
+ // The only thing that should have changed are addresses, type and checksum.
+ // Overwrite them with the received bytes and see if the packet is otherwise identical.
+ request[hdrLen] = buf[hdrLen]; // Type
+ request[hdrLen + 2] = buf[hdrLen + 2]; // Checksum byte 1.
+ request[hdrLen + 3] = buf[hdrLen + 3]; // Checksum byte 2.
+
+ // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore
+ // the request and reply may have different IPv6 flow label: ignore that as well.
+ if (version == 6) {
+ request[1] = (byte)(request[1] & 0xf0 | buf[1] & 0x0f);
+ request[2] = buf[2];
+ request[3] = buf[3];
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (buf[i] != request[i]) {
+ Log.i(TAG, "Received non-matching packet when expecting ping response.");
+ return;
+ }
+ }
+
+ // Now swap the addresses again and reflect the packet. This sends a ping reply.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+ }
+
+ private void writePacket(byte[] buf, int len) {
+ try {
+ Os.write(mFd, buf, 0, len);
+ } catch (ErrnoException|IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ private int readPacket(byte[] buf) {
+ int len;
+ try {
+ len = Os.read(mFd, buf, 0, buf.length);
+ } catch (ErrnoException|IOException e) {
+ Log.e(TAG, "Error reading packet: " + e.getMessage());
+ len = -1;
+ }
+ return len;
+ }
+
+ // Reads one packet from our mFd, and possibly writes the packet back.
+ private void processPacket() {
+ int len = readPacket(mBuf);
+ if (len < 1) {
+ return;
+ }
+
+ int version = mBuf[0] >> 4;
+ int addrPos, protoPos, hdrLen, addrLen;
+ if (version == 4) {
+ hdrLen = IPV4_HEADER_LENGTH;
+ protoPos = IPV4_PROTO_OFFSET;
+ addrPos = IPV4_ADDR_OFFSET;
+ addrLen = IPV4_ADDR_LENGTH;
+ } else if (version == 6) {
+ hdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ addrPos = IPV6_ADDR_OFFSET;
+ addrLen = IPV6_ADDR_LENGTH;
+ } else {
+ return;
+ }
+
+ if (len < hdrLen) {
+ return;
+ }
+
+ byte proto = mBuf[protoPos];
+ switch (proto) {
+ case IPPROTO_ICMP:
+ case IPPROTO_ICMPV6:
+ processIcmpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_TCP:
+ processTcpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_UDP:
+ processUdpPacket(mBuf, version, len, hdrLen);
+ break;
+ }
+ }
+
+ public void run() {
+ Log.i(TAG, "PacketReflector starting fd=" + mFd + " valid=" + mFd.valid());
+ while (!interrupted() && mFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "PacketReflector exiting fd=" + mFd + " valid=" + mFd.valid());
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java
new file mode 100644
index 0000000..18805f9
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java
@@ -0,0 +1,70 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
+import static com.android.cts.net.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/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
new file mode 100644
index 0000000..80f99b6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
@@ -0,0 +1,100 @@
+/*
+ * 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.net.hostside;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.cts.net.hostside.IRemoteSocketFactory;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+public class RemoteSocketFactoryClient {
+ private static final int TIMEOUT_MS = 5000;
+ private static final String PACKAGE = RemoteSocketFactoryClient.class.getPackage().getName();
+ private static final String APP2_PACKAGE = PACKAGE + ".app2";
+ private static final String SERVICE_NAME = APP2_PACKAGE + ".RemoteSocketFactoryService";
+
+ private Context mContext;
+ private ServiceConnection mServiceConnection;
+ private IRemoteSocketFactory mService;
+
+ public RemoteSocketFactoryClient(Context context) {
+ mContext = context;
+ }
+
+ public void bind() {
+ if (mService != null) {
+ throw new IllegalStateException("Already bound");
+ }
+
+ final ConditionVariable cv = new ConditionVariable();
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IRemoteSocketFactory.Stub.asInterface(service);
+ cv.open();
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+ };
+
+ final Intent intent = new Intent();
+ intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ cv.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to RemoteSocketFactory service after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ public void unbind() {
+ if (mService != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ }
+
+ public FileDescriptor openSocketFd(String host, int port, int timeoutMs)
+ throws RemoteException, ErrnoException, IOException {
+ // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
+ // and cause our fd to become invalid. http://b/35927643 .
+ ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs);
+ FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
+ pfd.close();
+ return fd;
+ }
+
+ public String getPackageName() throws RemoteException {
+ return mService.getPackageName();
+ }
+
+ public int getUid() throws RemoteException {
+ return mService.getUid();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java
new file mode 100644
index 0000000..96838bb
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java
@@ -0,0 +1,31 @@
+/*
+ * 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.net.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/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java
new file mode 100644
index 0000000..01f9f3e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java
@@ -0,0 +1,94 @@
+/*
+ * 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.net.hostside;
+
+import static com.android.cts.net.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/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
new file mode 100644
index 0000000..29d3c6e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.net.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();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ setRestrictedMode(false);
+ super.tearDown();
+ }
+
+ private void setRestrictedMode(boolean enabled) throws Exception {
+ executeSilentShellCommand(
+ "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
+ assertRestrictedModeState(enabled);
+ }
+
+ private void assertRestrictedModeState(boolean enabled) throws Exception {
+ assertDelayedShellCommand("cmd netpolicy get restricted-mode",
+ "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
+ }
+
+ @Test
+ public void testNetworkAccess() throws Exception {
+ setRestrictedMode(false);
+
+ // go to foreground state and enable restricted mode
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ setRestrictedMode(true);
+ assertForegroundNetworkAccess(false);
+
+ // go to background state
+ finishActivity();
+ assertBackgroundNetworkAccess(false);
+
+ // disable restricted mode and assert network access in foreground and background states
+ setRestrictedMode(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ assertForegroundNetworkAccess(true);
+
+ // go to background state
+ finishActivity();
+ assertBackgroundNetworkAccess(true);
+ }
+}
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
new file mode 100755
index 0000000..532fd86
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -0,0 +1,1248 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.os.Process.INVALID_UID;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.ECONNABORTED;
+import static android.system.OsConstants.IPPROTO_ICMP;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.POLLIN;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.test.MoreAsserts.assertNotEqual;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.annotation.Nullable;
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.Proxy;
+import android.net.ProxyInfo;
+import android.net.TransportInfo;
+import android.net.Uri;
+import android.net.VpnManager;
+import android.net.VpnService;
+import android.net.VpnTransportInfo;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiSelector;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructPollfd;
+import android.test.InstrumentationTestCase;
+import android.test.MoreAsserts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for the VpnService API.
+ *
+ * These tests establish a VPN via the VpnService API, and have the service reflect the packets back
+ * to the device without causing any network traffic. This allows testing the local VPN data path
+ * without a network connection or a VPN server.
+ *
+ * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these
+ * tests fail, it may be due to the lack of kernel support. The necessary patches can be
+ * cherry-picked from the Android common kernel trees:
+ *
+ * android-3.10:
+ * https://android-review.googlesource.com/#/c/99220/
+ * https://android-review.googlesource.com/#/c/100545/
+ *
+ * android-3.4:
+ * https://android-review.googlesource.com/#/c/99225/
+ * https://android-review.googlesource.com/#/c/100557/
+ *
+ * To ensure that the kernel has the required commits, run the kernel unit
+ * tests described at:
+ *
+ * https://source.android.com/devices/tech/config/kernel_network_tests.html
+ *
+ */
+public class VpnTest extends InstrumentationTestCase {
+
+ // These are neither public nor @TestApi.
+ // TODO: add them to @TestApi.
+ private static final String PRIVATE_DNS_MODE_SETTING = "private_dns_mode";
+ private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname";
+ private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+ private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier";
+
+ public static String TAG = "VpnTest";
+ public static int TIMEOUT_MS = 3 * 1000;
+ public static int SOCKET_TIMEOUT_MS = 100;
+ public static String TEST_HOST = "connectivitycheck.gstatic.com";
+
+ private UiDevice mDevice;
+ private MyActivity mActivity;
+ private String mPackageName;
+ private ConnectivityManager mCM;
+ private WifiManager mWifiManager;
+ private RemoteSocketFactoryClient mRemoteSocketFactoryClient;
+
+ Network mNetwork;
+ NetworkCallback mCallback;
+ final Object mLock = new Object();
+ final Object mLockShutdown = new Object();
+
+ private String mOldPrivateDnsMode;
+ private String mOldPrivateDnsSpecifier;
+
+ private boolean supportedHardware() {
+ final PackageManager pm = getInstrumentation().getContext().getPackageManager();
+ return !pm.hasSystemFeature("android.hardware.type.watch");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mNetwork = null;
+ mCallback = null;
+ storePrivateDnsSetting();
+
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
+ MyActivity.class, null);
+ mPackageName = mActivity.getPackageName();
+ mCM = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mWifiManager = (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE);
+ mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity);
+ mRemoteSocketFactoryClient.bind();
+ mDevice.waitForIdle();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ restorePrivateDnsSetting();
+ mRemoteSocketFactoryClient.unbind();
+ if (mCallback != null) {
+ mCM.unregisterNetworkCallback(mCallback);
+ }
+ Log.i(TAG, "Stopping VPN");
+ stopVpn();
+ mActivity.finish();
+ super.tearDown();
+ }
+
+ private void prepareVpn() throws Exception {
+ final int REQUEST_ID = 42;
+
+ // Attempt to prepare.
+ Log.i(TAG, "Preparing VPN");
+ Intent intent = VpnService.prepare(mActivity);
+
+ if (intent != null) {
+ // Start the confirmation dialog and click OK.
+ mActivity.startActivityForResult(intent, REQUEST_ID);
+ mDevice.waitForIdle();
+
+ String packageName = intent.getComponent().getPackageName();
+ String resourceIdRegex = "android:id/button1$|button_start_vpn";
+ final UiObject okButton = new UiObject(new UiSelector()
+ .className("android.widget.Button")
+ .packageName(packageName)
+ .resourceIdMatches(resourceIdRegex));
+ if (okButton.waitForExists(TIMEOUT_MS) == false) {
+ mActivity.finishActivity(REQUEST_ID);
+ fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " +
+ "to display the VPN confirmation dialog, but this test could not find the " +
+ "button to allow the VPN application to connect. Please ensure that the " +
+ "component displays a button with a resource ID matching the regexp: '" +
+ resourceIdRegex + "'.");
+ }
+
+ // Click the button and wait for RESULT_OK.
+ okButton.click();
+ try {
+ int result = mActivity.getResult(TIMEOUT_MS);
+ if (result != MyActivity.RESULT_OK) {
+ fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " +
+ "the button matching the regular expression '" + resourceIdRegex +
+ "' of " + intent.getComponent() + "'. Please ensure that clicking on " +
+ "that button allows the VPN application to connect. " +
+ "Return value: " + result);
+ }
+ } catch (InterruptedException e) {
+ fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms");
+ }
+
+ // Now we should be prepared.
+ intent = VpnService.prepare(mActivity);
+ if (intent != null) {
+ fail("VpnService.prepare returned non-null even after the VPN dialog " +
+ intent.getComponent() + "returned RESULT_OK.");
+ }
+ }
+ }
+
+ // TODO: Consider replacing arguments with a Builder.
+ private void startVpn(
+ String[] addresses, String[] routes, String allowedApplications,
+ String disallowedApplications, @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception {
+ prepareVpn();
+
+ // Register a callback so we will be notified when our VPN comes up.
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ mCallback = new NetworkCallback() {
+ public void onAvailable(Network network) {
+ synchronized (mLock) {
+ Log.i(TAG, "Got available callback for network=" + network);
+ mNetwork = network;
+ mLock.notify();
+ }
+ }
+ };
+ mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown.
+
+ // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
+ Intent intent = new Intent(mActivity, MyVpnService.class)
+ .putExtra(mPackageName + ".cmd", "connect")
+ .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
+ .putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+ .putExtra(mPackageName + ".allowedapplications", allowedApplications)
+ .putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
+ .putExtra(mPackageName + ".httpProxy", proxyInfo)
+ .putParcelableArrayListExtra(
+ mPackageName + ".underlyingNetworks", underlyingNetworks)
+ .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
+
+ mActivity.startService(intent);
+ synchronized (mLock) {
+ if (mNetwork == null) {
+ Log.i(TAG, "bf mLock");
+ mLock.wait(TIMEOUT_MS);
+ Log.i(TAG, "af mLock");
+ }
+ }
+
+ if (mNetwork == null) {
+ fail("VPN did not become available after " + TIMEOUT_MS + "ms");
+ }
+
+ // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
+ // configured. Give the system some time to do so. http://b/18436087 .
+ try { Thread.sleep(3000); } catch(InterruptedException e) {}
+ }
+
+ private void stopVpn() {
+ // Register a callback so we will be notified when our VPN comes up.
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ mCallback = new NetworkCallback() {
+ public void onLost(Network network) {
+ synchronized (mLockShutdown) {
+ Log.i(TAG, "Got lost callback for network=" + network
+ + ",mNetwork = " + mNetwork);
+ if( mNetwork == network){
+ mLockShutdown.notify();
+ }
+ }
+ }
+ };
+ mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown.
+ // Simply calling mActivity.stopService() won't stop the service, because the system binds
+ // to the service for the purpose of sending it a revoke command if another VPN comes up,
+ // and stopping a bound service has no effect. Instead, "start" the service again with an
+ // Intent that tells it to disconnect.
+ Intent intent = new Intent(mActivity, MyVpnService.class)
+ .putExtra(mPackageName + ".cmd", "disconnect");
+ mActivity.startService(intent);
+ synchronized (mLockShutdown) {
+ try {
+ Log.i(TAG, "bf mLockShutdown");
+ mLockShutdown.wait(TIMEOUT_MS);
+ Log.i(TAG, "af mLockShutdown");
+ } catch(InterruptedException e) {}
+ }
+ }
+
+ private static void closeQuietly(Closeable c) {
+ if (c != null) {
+ try {
+ c.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ private static void checkPing(String to) throws IOException, ErrnoException {
+ InetAddress address = InetAddress.getByName(to);
+ FileDescriptor s;
+ final int LENGTH = 64;
+ byte[] packet = new byte[LENGTH];
+ byte[] header;
+
+ // Construct a ping packet.
+ Random random = new Random();
+ random.nextBytes(packet);
+ if (address instanceof Inet6Address) {
+ s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+ header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ } else {
+ // Note that this doesn't actually work due to http://b/18558481 .
+ s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
+ header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ }
+ System.arraycopy(header, 0, packet, 0, header.length);
+
+ // Send the packet.
+ int port = random.nextInt(65534) + 1;
+ Os.connect(s, address, port);
+ Os.write(s, packet, 0, packet.length);
+
+ // Expect a reply.
+ StructPollfd pollfd = new StructPollfd();
+ pollfd.events = (short) POLLIN; // "error: possible loss of precision"
+ pollfd.fd = s;
+ int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS);
+ assertEquals("Expected reply after sending ping", 1, ret);
+
+ byte[] reply = new byte[LENGTH];
+ int read = Os.read(s, reply, 0, LENGTH);
+ assertEquals(LENGTH, read);
+
+ // Find out what the kernel set the ICMP ID to.
+ InetSocketAddress local = (InetSocketAddress) Os.getsockname(s);
+ port = local.getPort();
+ packet[4] = (byte) ((port >> 8) & 0xff);
+ packet[5] = (byte) (port & 0xff);
+
+ // Check the contents.
+ if (packet[0] == (byte) 0x80) {
+ packet[0] = (byte) 0x81;
+ } else {
+ packet[0] = 0;
+ }
+ // Zero out the checksum in the reply so it matches the uninitialized checksum in packet.
+ reply[2] = reply[3] = 0;
+ MoreAsserts.assertEquals(packet, reply);
+ }
+
+ // Writes data to out and checks that it appears identically on in.
+ private static void writeAndCheckData(
+ OutputStream out, InputStream in, byte[] data) throws IOException {
+ out.write(data, 0, data.length);
+ out.flush();
+
+ byte[] read = new byte[data.length];
+ int bytesRead = 0, totalRead = 0;
+ do {
+ bytesRead = in.read(read, totalRead, read.length - totalRead);
+ totalRead += bytesRead;
+ } while (bytesRead >= 0 && totalRead < data.length);
+ assertEquals(totalRead, data.length);
+ MoreAsserts.assertEquals(data, read);
+ }
+
+ private void checkTcpReflection(String to, String expectedFrom) throws IOException {
+ // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a
+ // client socket, and connect the client socket to a remote host, with the port of the
+ // server socket. The PacketReflector reflects the packets, changing the source addresses
+ // but not the ports, so our client socket is connected to our server socket, though both
+ // sockets think their peers are on the "remote" IP address.
+
+ // Open a listening socket.
+ ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::"));
+
+ // Connect the client socket to it.
+ InetAddress toAddr = InetAddress.getByName(to);
+ Socket client = new Socket();
+ try {
+ client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS);
+ if (expectedFrom == null) {
+ closeQuietly(listen);
+ closeQuietly(client);
+ fail("Expected connection to fail, but it succeeded.");
+ }
+ } catch (IOException e) {
+ if (expectedFrom != null) {
+ closeQuietly(listen);
+ fail("Expected connection to succeed, but it failed.");
+ } else {
+ // We expected the connection to fail, and it did, so there's nothing more to test.
+ return;
+ }
+ }
+
+ // The connection succeeded, and we expected it to succeed. Send some data; if things are
+ // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive
+ // at our server socket. For good measure, send some data in the other direction.
+ Socket server = null;
+ try {
+ // Accept the connection on the server side.
+ listen.setSoTimeout(SOCKET_TIMEOUT_MS);
+ server = listen.accept();
+ checkConnectionOwnerUidTcp(client);
+ checkConnectionOwnerUidTcp(server);
+ // Check that the source and peer addresses are as expected.
+ assertEquals(expectedFrom, client.getLocalAddress().getHostAddress());
+ assertEquals(expectedFrom, server.getLocalAddress().getHostAddress());
+ assertEquals(
+ new InetSocketAddress(toAddr, client.getLocalPort()),
+ server.getRemoteSocketAddress());
+ assertEquals(
+ new InetSocketAddress(toAddr, server.getLocalPort()),
+ client.getRemoteSocketAddress());
+
+ // Now write some data.
+ final int LENGTH = 32768;
+ byte[] data = new byte[LENGTH];
+ new Random().nextBytes(data);
+
+ // Make sure our writes don't block or time out, because we're single-threaded and can't
+ // read and write at the same time.
+ server.setReceiveBufferSize(LENGTH * 2);
+ client.setSendBufferSize(LENGTH * 2);
+ client.setSoTimeout(SOCKET_TIMEOUT_MS);
+ server.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+ // Send some data from client to server, then from server to client.
+ writeAndCheckData(client.getOutputStream(), server.getInputStream(), data);
+ writeAndCheckData(server.getOutputStream(), client.getInputStream(), data);
+ } finally {
+ closeQuietly(listen);
+ closeQuietly(client);
+ closeQuietly(server);
+ }
+ }
+
+ private void checkConnectionOwnerUidUdp(DatagramSocket s, boolean expectSuccess) {
+ final int expectedUid = expectSuccess ? Process.myUid() : INVALID_UID;
+ InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
+ InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
+ int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_UDP, loc, rem);
+ assertEquals(expectedUid, uid);
+ }
+
+ private void checkConnectionOwnerUidTcp(Socket s) {
+ final int expectedUid = Process.myUid();
+ InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
+ InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
+ int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
+ assertEquals(expectedUid, uid);
+ }
+
+ private void checkUdpEcho(String to, String expectedFrom) throws IOException {
+ DatagramSocket s;
+ InetAddress address = InetAddress.getByName(to);
+ if (address instanceof Inet6Address) { // http://b/18094870
+ s = new DatagramSocket(0, InetAddress.getByName("::"));
+ } else {
+ s = new DatagramSocket();
+ }
+ s.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+ Random random = new Random();
+ byte[] data = new byte[random.nextInt(1650)];
+ random.nextBytes(data);
+ DatagramPacket p = new DatagramPacket(data, data.length);
+ s.connect(address, 7);
+
+ if (expectedFrom != null) {
+ assertEquals("Unexpected source address: ",
+ expectedFrom, s.getLocalAddress().getHostAddress());
+ }
+
+ try {
+ if (expectedFrom != null) {
+ s.send(p);
+ checkConnectionOwnerUidUdp(s, true);
+ s.receive(p);
+ MoreAsserts.assertEquals(data, p.getData());
+ } else {
+ try {
+ s.send(p);
+ s.receive(p);
+ fail("Received unexpected reply");
+ } catch (IOException expected) {
+ checkConnectionOwnerUidUdp(s, false);
+ }
+ }
+ } finally {
+ s.close();
+ }
+ }
+
+ private void checkTrafficOnVpn() throws Exception {
+ checkUdpEcho("192.0.2.251", "192.0.2.2");
+ checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+ checkPing("2001:db8:dead:beef::f00");
+ checkTcpReflection("192.0.2.252", "192.0.2.2");
+ checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+ }
+
+ private void checkNoTrafficOnVpn() throws Exception {
+ checkUdpEcho("192.0.2.251", null);
+ checkUdpEcho("2001:db8:dead:beef::f00", null);
+ checkTcpReflection("192.0.2.252", null);
+ checkTcpReflection("2001:db8:dead:beef::f00", null);
+ }
+
+ private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
+ Socket s = new Socket(host, port);
+ s.setSoTimeout(timeoutMs);
+ // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
+ // and cause our fd to become invalid. http://b/35927643 .
+ FileDescriptor fd = Os.dup(ParcelFileDescriptor.fromSocket(s).getFileDescriptor());
+ s.close();
+ return fd;
+ }
+
+ private FileDescriptor openSocketFdInOtherApp(
+ String host, int port, int timeoutMs) throws Exception {
+ Log.d(TAG, String.format("Creating test socket in UID=%d, my UID=%d",
+ mRemoteSocketFactoryClient.getUid(), Os.getuid()));
+ FileDescriptor fd = mRemoteSocketFactoryClient.openSocketFd(host, port, TIMEOUT_MS);
+ return fd;
+ }
+
+ private void sendRequest(FileDescriptor fd, String host) throws Exception {
+ String request = "GET /generate_204 HTTP/1.1\r\n" +
+ "Host: " + host + "\r\n" +
+ "Connection: keep-alive\r\n\r\n";
+ byte[] requestBytes = request.getBytes(StandardCharsets.UTF_8);
+ int ret = Os.write(fd, requestBytes, 0, requestBytes.length);
+ Log.d(TAG, "Wrote " + ret + "bytes");
+
+ String expected = "HTTP/1.1 204 No Content\r\n";
+ byte[] response = new byte[expected.length()];
+ Os.read(fd, response, 0, response.length);
+
+ String actual = new String(response, StandardCharsets.UTF_8);
+ assertEquals(expected, actual);
+ Log.d(TAG, "Got response: " + actual);
+ }
+
+ private void assertSocketStillOpen(FileDescriptor fd, String host) throws Exception {
+ try {
+ assertTrue(fd.valid());
+ sendRequest(fd, host);
+ assertTrue(fd.valid());
+ } finally {
+ Os.close(fd);
+ }
+ }
+
+ private void assertSocketClosed(FileDescriptor fd, String host) throws Exception {
+ try {
+ assertTrue(fd.valid());
+ sendRequest(fd, host);
+ fail("Socket opened before VPN connects should be closed when VPN connects");
+ } catch (ErrnoException expected) {
+ assertEquals(ECONNABORTED, expected.errno);
+ assertTrue(fd.valid());
+ } finally {
+ Os.close(fd);
+ }
+ }
+
+ private ContentResolver getContentResolver() {
+ return getInstrumentation().getContext().getContentResolver();
+ }
+
+ private boolean isPrivateDnsInStrictMode() {
+ return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(
+ Settings.Global.getString(getContentResolver(), PRIVATE_DNS_MODE_SETTING));
+ }
+
+ private void storePrivateDnsSetting() {
+ mOldPrivateDnsMode = Settings.Global.getString(getContentResolver(),
+ PRIVATE_DNS_MODE_SETTING);
+ mOldPrivateDnsSpecifier = Settings.Global.getString(getContentResolver(),
+ PRIVATE_DNS_SPECIFIER_SETTING);
+ }
+
+ private void restorePrivateDnsSetting() {
+ Settings.Global.putString(getContentResolver(), PRIVATE_DNS_MODE_SETTING,
+ mOldPrivateDnsMode);
+ Settings.Global.putString(getContentResolver(), PRIVATE_DNS_SPECIFIER_SETTING,
+ mOldPrivateDnsSpecifier);
+ }
+
+ // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above.
+ private void expectPrivateDnsHostname(final String hostname) throws Exception {
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .build();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
+ if (network.equals(mNetwork) &&
+ Objects.equals(lp.getPrivateDnsServerName(), hostname)) {
+ latch.countDown();
+ }
+ }
+ };
+
+ mCM.registerNetworkCallback(request, callback);
+
+ try {
+ assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ mCM.unregisterNetworkCallback(callback);
+ }
+ }
+
+ private void setAndVerifyPrivateDns(boolean strictMode) throws Exception {
+ final ContentResolver cr = getInstrumentation().getContext().getContentResolver();
+ String privateDnsHostname;
+
+ if (strictMode) {
+ privateDnsHostname = "vpncts-nx.metric.gstatic.com";
+ Settings.Global.putString(cr, PRIVATE_DNS_SPECIFIER_SETTING, privateDnsHostname);
+ Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING,
+ PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
+ } else {
+ Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, PRIVATE_DNS_MODE_OPPORTUNISTIC);
+ privateDnsHostname = null;
+ }
+
+ expectPrivateDnsHostname(privateDnsHostname);
+
+ String randomName = "vpncts-" + new Random().nextInt(1000000000) + "-ds.metric.gstatic.com";
+ if (strictMode) {
+ // Strict mode private DNS is enabled. DNS lookups should fail, because the private DNS
+ // server name is invalid.
+ try {
+ InetAddress.getByName(randomName);
+ fail("VPN DNS lookup should fail with private DNS enabled");
+ } catch (UnknownHostException expected) {
+ }
+ } else {
+ // Strict mode private DNS is disabled. DNS lookup should succeed, because the VPN
+ // provides no DNS servers, and thus DNS falls through to the default network.
+ assertNotNull("VPN DNS lookup should succeed with private DNS disabled",
+ InetAddress.getByName(randomName));
+ }
+ }
+
+ // Tests that strict mode private DNS is used on VPNs.
+ private void checkStrictModePrivateDns() throws Exception {
+ final boolean initialMode = isPrivateDnsInStrictMode();
+ setAndVerifyPrivateDns(!initialMode);
+ setAndVerifyPrivateDns(initialMode);
+ }
+
+ public void testDefault() throws Exception {
+ if (!supportedHardware()) return;
+ // If adb TCP port opened, this test may running by adb over network.
+ // All of socket would be destroyed in this test. So this test don't
+ // support adb over network, see b/119382723.
+ if (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
+ || SystemProperties.getInt("service.adb.tcp.port", -1) > -1) {
+ Log.i(TAG, "adb is running over the network, so skip this test");
+ return;
+ }
+
+ final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(
+ getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED);
+ receiver.register();
+
+ // Test the behaviour of a variety of types of network callbacks.
+ final Network defaultNetwork = mCM.getActiveNetwork();
+ final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
+ final TestableNetworkCallback otherUidCallback = new TestableNetworkCallback();
+ final TestableNetworkCallback myUidCallback = new TestableNetworkCallback();
+ if (SdkLevel.isAtLeastS()) {
+ final int otherUid =
+ UserHandle.of(5 /* userId */).getUid(Process.FIRST_APPLICATION_UID);
+ final Handler h = new Handler(Looper.getMainLooper());
+ runWithShellPermissionIdentity(() -> {
+ mCM.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
+ mCM.registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, h);
+ mCM.registerDefaultNetworkCallbackForUid(Process.myUid(), myUidCallback, h);
+ }, NETWORK_SETTINGS);
+ for (TestableNetworkCallback callback :
+ List.of(systemDefaultCallback, otherUidCallback, myUidCallback)) {
+ callback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+ true /* validated */, false /* blocked */, TIMEOUT_MS);
+ }
+ }
+
+ FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"},
+ "", "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ final Intent intent = receiver.awaitForBroadcast(TimeUnit.MINUTES.toMillis(1));
+ assertNotNull("Failed to receive broadcast from VPN service", intent);
+ assertFalse("Wrong VpnService#isAlwaysOn",
+ intent.getBooleanExtra(MyVpnService.EXTRA_ALWAYS_ON, true));
+ assertFalse("Wrong VpnService#isLockdownEnabled",
+ intent.getBooleanExtra(MyVpnService.EXTRA_LOCKDOWN_ENABLED, true));
+
+ assertSocketClosed(fd, TEST_HOST);
+
+ checkTrafficOnVpn();
+
+ final Network vpnNetwork = mCM.getActiveNetwork();
+ myUidCallback.expectAvailableThenValidatedCallbacks(vpnNetwork, TIMEOUT_MS);
+ assertEquals(vpnNetwork, mCM.getActiveNetwork());
+ assertNotEqual(defaultNetwork, vpnNetwork);
+ maybeExpectVpnTransportInfo(vpnNetwork);
+
+ if (SdkLevel.isAtLeastS()) {
+ // Check that system default network callback has not seen any network changes, even
+ // though the app's default network changed. Also check that otherUidCallback saw no
+ // network changes, because otherUid is in a different user and not subject to the VPN.
+ // This needs to be done before testing private DNS because checkStrictModePrivateDns
+ // will set the private DNS server to a nonexistent name, which will cause validation to
+ // fail and could cause the default network to switch (e.g., from wifi to cellular).
+ systemDefaultCallback.assertNoCallback();
+ otherUidCallback.assertNoCallback();
+ mCM.unregisterNetworkCallback(systemDefaultCallback);
+ mCM.unregisterNetworkCallback(otherUidCallback);
+ mCM.unregisterNetworkCallback(myUidCallback);
+ }
+
+ checkStrictModePrivateDns();
+
+ receiver.unregisterQuietly();
+ }
+
+ public void testAppAllowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ allowedApps, "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ assertSocketClosed(fd, TEST_HOST);
+
+ checkTrafficOnVpn();
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+
+ checkStrictModePrivateDns();
+ }
+
+ public void testAppDisallowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
+ FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
+
+ String disallowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ // If adb TCP port opened, this test may running by adb over TCP.
+ // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test,
+ // see b/119382723.
+ // Note: The test don't support running adb over network for root device
+ disallowedApps = disallowedApps + ",com.android.shell";
+ Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps);
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ "", disallowedApps, null, null /* underlyingNetworks */,
+ false /* isAlwaysMetered */);
+
+ assertSocketStillOpen(localFd, TEST_HOST);
+ assertSocketStillOpen(remoteFd, TEST_HOST);
+
+ checkNoTrafficOnVpn();
+
+ final Network network = mCM.getActiveNetwork();
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ assertFalse(nc.hasTransport(TRANSPORT_VPN));
+ }
+
+ public void testGetConnectionOwnerUidSecurity() throws Exception {
+ if (!supportedHardware()) return;
+
+ DatagramSocket s;
+ InetAddress address = InetAddress.getByName("localhost");
+ s = new DatagramSocket();
+ s.setSoTimeout(SOCKET_TIMEOUT_MS);
+ s.connect(address, 7);
+ InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
+ InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
+ try {
+ int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
+ assertEquals("Only an active VPN app should see connection information",
+ INVALID_UID, uid);
+ } catch (SecurityException acceptable) {
+ // R and below throw SecurityException if a non-active VPN calls this method.
+ // As long as we can't actually get socket information, either behaviour is fine.
+ return;
+ }
+ }
+
+ public void testSetProxy() throws Exception {
+ if (!supportedHardware()) return;
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+ // Receiver for the proxy change broadcast.
+ BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+
+ String allowedApps = mPackageName;
+ ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "",
+ testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // Check that the proxy change broadcast is received
+ try {
+ assertNotNull("No proxy change was broadcast when VPN is connected.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+
+ // Proxy is set correctly in network and in link properties.
+ assertNetworkHasExpectedProxy(testProxyInfo, mNetwork);
+ assertDefaultProxy(testProxyInfo);
+
+ proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ stopVpn();
+ try {
+ assertNotNull("No proxy change was broadcast when VPN was disconnected.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+
+ // After disconnecting from VPN, the proxy settings are the ones of the initial network.
+ assertDefaultProxy(initialProxy);
+ }
+
+ public void testSetProxyDisallowedApps() throws Exception {
+ if (!supportedHardware()) return;
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+
+ // If adb TCP port opened, this test may running by adb over TCP.
+ // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test,
+ // see b/119382723.
+ // Note: The test don't support running adb over network for root device
+ String disallowedApps = mPackageName + ",com.android.shell";
+ ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, "", disallowedApps,
+ testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // The disallowed app does has the proxy configs of the default network.
+ assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
+ assertDefaultProxy(initialProxy);
+ }
+
+ public void testNoProxy() throws Exception {
+ if (!supportedHardware()) return;
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+ BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ String allowedApps = mPackageName;
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ try {
+ assertNotNull("No proxy change was broadcast.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+
+ // The VPN network has no proxy set.
+ assertNetworkHasExpectedProxy(null, mNetwork);
+
+ proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ stopVpn();
+ try {
+ assertNotNull("No proxy change was broadcast.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+ // After disconnecting from VPN, the proxy settings are the ones of the initial network.
+ assertDefaultProxy(initialProxy);
+ assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
+ }
+
+ public void testBindToNetworkWithProxy() throws Exception {
+ if (!supportedHardware()) return;
+ String allowedApps = mPackageName;
+ Network initialNetwork = mCM.getActiveNetwork();
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+ ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
+ // Receiver for the proxy change broadcast.
+ BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "",
+ testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ assertDefaultProxy(testProxyInfo);
+ mCM.bindProcessToNetwork(initialNetwork);
+ try {
+ assertNotNull("No proxy change was broadcast.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+ assertDefaultProxy(initialProxy);
+ }
+
+ public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ // VPN is not routing any traffic i.e. its underlying networks is an empty array.
+ ArrayList<Network> underlyingNetworks = new ArrayList<>();
+ String allowedApps = mPackageName;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, false /* isAlwaysMetered */);
+
+ // VPN should now be the active network.
+ assertEquals(mNetwork, mCM.getActiveNetwork());
+ assertVpnTransportContains(NetworkCapabilities.TRANSPORT_VPN);
+ // VPN with no underlying networks should be metered by default.
+ assertTrue(isNetworkMetered(mNetwork));
+ assertTrue(mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+ }
+
+ public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN tracks platform default.
+ ArrayList<Network> underlyingNetworks = null;
+ String allowedApps = mPackageName;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, false /*isAlwaysMetered */);
+
+ // Ensure VPN transports contains underlying network's transports.
+ assertVpnTransportContains(underlyingNetwork);
+ // Its meteredness should be same as that of underlying network.
+ assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
+ // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
+ assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+ }
+
+ public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN explicitly declares WiFi to be its underlying network.
+ ArrayList<Network> underlyingNetworks = new ArrayList<>(1);
+ underlyingNetworks.add(underlyingNetwork);
+ String allowedApps = mPackageName;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, false /* isAlwaysMetered */);
+
+ // Ensure VPN transports contains underlying network's transports.
+ assertVpnTransportContains(underlyingNetwork);
+ // Its meteredness should be same as that of underlying network.
+ assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
+ // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
+ assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+ }
+
+ public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN tracks platform default.
+ ArrayList<Network> underlyingNetworks = null;
+ String allowedApps = mPackageName;
+ boolean isAlwaysMetered = true;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, isAlwaysMetered);
+
+ // VPN's meteredness does not depend on underlying network since it is always metered.
+ assertTrue(isNetworkMetered(mNetwork));
+ assertTrue(mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+ }
+
+ public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN explicitly declares its underlying network.
+ ArrayList<Network> underlyingNetworks = new ArrayList<>(1);
+ underlyingNetworks.add(underlyingNetwork);
+ String allowedApps = mPackageName;
+ boolean isAlwaysMetered = true;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, isAlwaysMetered);
+
+ // VPN's meteredness does not depend on underlying network since it is always metered.
+ assertTrue(isNetworkMetered(mNetwork));
+ assertTrue(mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+ }
+
+ public void testB141603906() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ final InetSocketAddress src = new InetSocketAddress(0);
+ final InetSocketAddress dst = new InetSocketAddress(0);
+ final int NUM_THREADS = 8;
+ final int NUM_SOCKETS = 5000;
+ final Thread[] threads = new Thread[NUM_THREADS];
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"},
+ "" /* allowedApplications */, "com.android.shell" /* disallowedApplications */,
+ null /* proxyInfo */, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ for (int i = 0; i < NUM_THREADS; i++) {
+ threads[i] = new Thread(() -> {
+ for (int j = 0; j < NUM_SOCKETS; j++) {
+ mCM.getConnectionOwnerUid(IPPROTO_TCP, src, dst);
+ }
+ });
+ }
+ for (Thread thread : threads) {
+ thread.start();
+ }
+ for (Thread thread : threads) {
+ thread.join();
+ }
+ stopVpn();
+ }
+
+ private boolean isNetworkMetered(Network network) {
+ NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ private void assertVpnTransportContains(Network underlyingNetwork) {
+ int[] transports = mCM.getNetworkCapabilities(underlyingNetwork).getTransportTypes();
+ assertVpnTransportContains(transports);
+ }
+
+ private void assertVpnTransportContains(int... transports) {
+ NetworkCapabilities vpnCaps = mCM.getNetworkCapabilities(mNetwork);
+ for (int transport : transports) {
+ assertTrue(vpnCaps.hasTransport(transport));
+ }
+ }
+
+ private void maybeExpectVpnTransportInfo(Network network) {
+ if (!SdkLevel.isAtLeastS()) return;
+ final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
+ assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
+ final TransportInfo ti = vpnNc.getTransportInfo();
+ assertTrue(ti instanceof VpnTransportInfo);
+ assertEquals(VpnManager.TYPE_VPN_SERVICE, ((VpnTransportInfo) ti).getType());
+ }
+
+ private void assertDefaultProxy(ProxyInfo expected) {
+ assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy());
+ String expectedHost = expected == null ? null : expected.getHost();
+ String expectedPort = expected == null ? null : String.valueOf(expected.getPort());
+ assertEquals("Incorrect proxy host system property.", expectedHost,
+ System.getProperty("http.proxyHost"));
+ assertEquals("Incorrect proxy port system property.", expectedPort,
+ System.getProperty("http.proxyPort"));
+ }
+
+ private void assertNetworkHasExpectedProxy(ProxyInfo expected, Network network) {
+ LinkProperties lp = mCM.getLinkProperties(network);
+ assertNotNull("The network link properties object is null.", lp);
+ assertEquals("Incorrect proxy config.", expected, lp.getHttpProxy());
+
+ assertEquals(expected, mCM.getProxyForNetwork(network));
+ }
+
+ class ProxyChangeBroadcastReceiver extends BlockingBroadcastReceiver {
+ private boolean received;
+
+ public ProxyChangeBroadcastReceiver() {
+ super(VpnTest.this.getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION);
+ received = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!received) {
+ // Do not call onReceive() more than once.
+ super.onReceive(context, intent);
+ }
+ received = true;
+ }
+ }
+
+ /**
+ * Verifies that DownloadManager has CONNECTIVITY_USE_RESTRICTED_NETWORKS permission that can
+ * bind socket to VPN when it is in VPN disallowed list but requested downloading app is in VPN
+ * allowed list.
+ * See b/165774987.
+ */
+ public void testDownloadWithDownloadManagerDisallowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ // Start a VPN with DownloadManager package in disallowed list.
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ "" /* allowedApps */, "com.android.providers.downloads", null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ final Context context = VpnTest.this.getInstrumentation().getContext();
+ final DownloadManager dm = context.getSystemService(DownloadManager.class);
+ final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
+ try {
+ context.registerReceiver(receiver,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
+
+ // Enqueue a request and check only one download.
+ final long id = dm.enqueue(new Request(Uri.parse("https://www.google.com")));
+ assertEquals(1, getTotalNumberDownloads(dm, new Query()));
+ assertEquals(1, getTotalNumberDownloads(dm, new Query().setFilterById(id)));
+
+ // Wait for download complete and check status.
+ assertEquals(id, receiver.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(1, getTotalNumberDownloads(dm,
+ new Query().setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)));
+
+ // Remove download.
+ assertEquals(1, dm.remove(id));
+ assertEquals(0, getTotalNumberDownloads(dm, new Query()));
+ } finally {
+ context.unregisterReceiver(receiver);
+ }
+ }
+
+ private static int getTotalNumberDownloads(final DownloadManager dm, final Query query) {
+ try (Cursor cursor = dm.query(query)) { return cursor.getCount(); }
+ }
+
+ private static class DownloadCompleteReceiver extends BroadcastReceiver {
+ private final CompletableFuture<Long> future = new CompletableFuture<>();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ future.complete(intent.getLongExtra(
+ DownloadManager.EXTRA_DOWNLOAD_ID, -1 /* defaultValue */));
+ }
+
+ public long get(long timeout, TimeUnit unit) throws Exception {
+ return future.get(timeout, unit);
+ }
+ }
+}
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
new file mode 100644
index 0000000..dd33eed
--- /dev/null
+++ b/tests/cts/hostside/app2/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp2",
+ defaults: ["cts_support_defaults"],
+ sdk_version: "test_current",
+ static_libs: ["CtsHostsideNetworkTestsAidl"],
+ srcs: ["src/**/*.java"],
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ certificate: ":cts-net-app",
+}
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
new file mode 100644
index 0000000..4ac4bcb
--- /dev/null
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.net.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.INTERNET"/>
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+
+ <!--
+ 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:exported="true"/>
+ <service android:name=".RemoteSocketFactoryService"
+ android:exported="true"/>
+
+ <receiver android:name=".MyBroadcastReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED"/>
+ <action android:name="com.android.cts.net.hostside.app2.action.GET_COUNTERS"/>
+ <action android:name="com.android.cts.net.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS"/>
+ <action android:name="com.android.cts.net.hostside.app2.action.CHECK_NETWORK"/>
+ <action android:name="com.android.cts.net.hostside.app2.action.SEND_NOTIFICATION"/>
+ <action android:name="com.android.cts.net.hostside.app2.action.SHOW_TOAST"/>
+ </intent-filter>
+ </receiver>
+ <service android:name=".MyJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+ </application>
+
+</manifest>
diff --git a/tests/cts/hostside/app2/res/drawable/ic_notification.png b/tests/cts/hostside/app2/res/drawable/ic_notification.png
new file mode 100644
index 0000000..6ae570b
--- /dev/null
+++ b/tests/cts/hostside/app2/res/drawable/ic_notification.png
Binary files differ
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
new file mode 100644
index 0000000..62b508c
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
@@ -0,0 +1,149 @@
+/*
+ * 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.net.hostside.app2;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.net.hostside.INetworkStateObserver;
+
+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.net.hostside.app2.action.RECEIVER_READY";
+ static final String ACTION_FINISH_ACTIVITY =
+ "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY";
+ static final String ACTION_FINISH_JOB =
+ "com.android.cts.net.hostside.app2.action.FINISH_JOB";
+ static final String ACTION_SHOW_TOAST =
+ "com.android.cts.net.hostside.app2.action.SHOW_TOAST";
+
+ 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.net.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 int TYPE_COMPONENT_ACTIVTY = 0;
+ static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
+ static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
+
+ 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 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(
+ INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
+ "Unexpected procstate: " + procState);
+ 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(
+ INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
+ "Unexpected procstate: " + procState);
+ return false;
+ }
+ return true;
+ }
+ case TYPE_COMPONENT_EXPEDITED_JOB: {
+ final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
+ if ((capabilities & ActivityManager.PROCESS_CAPABILITY_NETWORK) == 0) {
+ observer.onNetworkStateChecked(
+ INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES,
+ "Unexpected capabilities: " + capabilities);
+ return false;
+ }
+ return true;
+ }
+ default: {
+ observer.onNetworkStateChecked(INetworkStateObserver.RESULT_ERROR_OTHER,
+ "Unknown component type: " + componentType);
+ 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) {
+ 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,
+ MyBroadcastReceiver.checkNetworkStatus(context));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error occurred while notifying the observer: " + e);
+ }
+ });
+ }
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
new file mode 100644
index 0000000..9fdb9c9
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -0,0 +1,76 @@
+/*
+ * 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.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
+import static com.android.cts.net.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.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.net.hostside.INetworkStateObserver;
+
+/**
+ * Activity used to bring process to foreground.
+ */
+public class MyActivity extends Activity {
+
+ private BroadcastReceiver finishCommandReceiver = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "MyActivity.onCreate()");
+ Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
+ 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));
+ }
+
+ @Override
+ public void finish() {
+ if (finishCommandReceiver != null) {
+ unregisterReceiver(finishCommandReceiver);
+ }
+ super.finish();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Log.d(TAG, "MyActivity.onStart()");
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.d(TAG, "MyActivity.onDestroy()");
+ super.onDestroy();
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
new file mode 100644
index 0000000..c9ae16f
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
@@ -0,0 +1,267 @@
+/*
+ * 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.net.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.net.hostside.app2.Common.ACTION_SHOW_TOAST;
+import static com.android.cts.net.hostside.app2.Common.MANIFEST_RECEIVER;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.hostside.app2.Common.getUid;
+
+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.net.NetworkInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * 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 static final int NETWORK_TIMEOUT_MS = 5 * 1000;
+
+ 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_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);
+ }
+
+ private static final String NETWORK_STATUS_TEMPLATE = "%s|%s|%s|%s|%s";
+ /**
+ * Checks whether the network is available and return a string which can then be send as a
+ * result data for the ordered broadcast.
+ *
+ * <p>
+ * The string has the following format:
+ *
+ * <p><pre><code>
+ * NetinfoState|NetinfoDetailedState|RealConnectionCheck|RealConnectionCheckDetails|Netinfo
+ * </code></pre>
+ *
+ * <p>Where:
+ *
+ * <ul>
+ * <li>{@code NetinfoState}: enum value of {@link NetworkInfo.State}.
+ * <li>{@code NetinfoDetailedState}: enum value of {@link NetworkInfo.DetailedState}.
+ * <li>{@code RealConnectionCheck}: boolean value of a real connection check (i.e., an attempt
+ * to access an external website.
+ * <li>{@code RealConnectionCheckDetails}: if HTTP output core or exception string of the real
+ * connection attempt
+ * <li>{@code Netinfo}: string representation of the {@link NetworkInfo}.
+ * </ul>
+ *
+ * For example, if the connection was established fine, the result would be something like:
+ * <p><pre><code>
+ * CONNECTED|CONNECTED|true|200|[type: WIFI[], state: CONNECTED/CONNECTED, reason: ...]
+ * </code></pre>
+ *
+ */
+ // TODO: now that it uses Binder, it counl return a Bundle with the data parts instead...
+ static String checkNetworkStatus(Context context) {
+ final ConnectivityManager cm =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ // TODO: connect to a hostside server instead
+ final String address = "http://example.com";
+ 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.setDoInput(true);
+ 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;
+ }
+ Log.d(TAG, checkDetails);
+ final String state, detailedState;
+ if (networkInfo != null) {
+ state = networkInfo.getState().name();
+ detailedState = networkInfo.getDetailedState().name();
+ } else {
+ state = detailedState = "null";
+ }
+ final String status = String.format(NETWORK_STATUS_TEMPLATE, state, detailedState,
+ Boolean.valueOf(checkStatus), checkDetails, networkInfo);
+ Log.d(TAG, "Offering " + status);
+ return status;
+ }
+
+ /**
+ * 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/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
new file mode 100644
index 0000000..b55761c
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
@@ -0,0 +1,73 @@
+/*
+ * 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.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
+import static com.android.cts.net.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.net.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/app2/src/com/android/cts/net/hostside/app2/MyJobService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
new file mode 100644
index 0000000..51c3157
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_JOB;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.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));
+ 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/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
new file mode 100644
index 0000000..7dc4b9c
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -0,0 +1,195 @@
+/*
+ * 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.net.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER;
+import static com.android.cts.net.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.net.hostside.IMyService;
+import com.android.cts.net.hostside.INetworkCallback;
+
+/**
+ * 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();
+ mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER);
+ context.registerReceiver(mReceiver, new IntentFilter(ACTION_RECEIVER_READY));
+ context.registerReceiver(mReceiver,
+ new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED));
+ Log.d(TAG, "receiver registered");
+ }
+
+ @Override
+ public int getCounters(String receiverName, String action) {
+ return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName);
+ }
+
+ @Override
+ public String checkNetworkStatus() {
+ return MyBroadcastReceiver.checkNetworkStatus(getApplicationContext());
+ }
+
+ @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 void scheduleJob(JobInfo jobInfo) {
+ final JobScheduler jobScheduler = getApplicationContext()
+ .getSystemService(JobScheduler.class);
+ 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/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
new file mode 100644
index 0000000..b1b7d77
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.net.hostside.app2;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.cts.net.hostside.IRemoteSocketFactory;
+
+import java.net.Socket;
+
+
+public class RemoteSocketFactoryService extends Service {
+
+ private static final String TAG = RemoteSocketFactoryService.class.getSimpleName();
+
+ private IRemoteSocketFactory.Stub mBinder = new IRemoteSocketFactory.Stub() {
+ @Override
+ public ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs) {
+ try {
+ Socket s = new Socket(host, port);
+ s.setSoTimeout(timeoutMs);
+ return ParcelFileDescriptor.fromSocket(s);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public String getPackageName() {
+ return RemoteSocketFactoryService.this.getPackageName();
+ }
+
+ @Override
+ public int getUid() {
+ return Process.myUid();
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
new file mode 100644
index 0000000..60b5476
--- /dev/null
+++ b/tests/cts/hostside/certs/Android.bp
@@ -0,0 +1,8 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app_certificate {
+ name: "cts-net-app",
+ certificate: "cts-net-app",
+}
diff --git a/tests/cts/hostside/certs/README b/tests/cts/hostside/certs/README
new file mode 100644
index 0000000..b660a82
--- /dev/null
+++ b/tests/cts/hostside/certs/README
@@ -0,0 +1,2 @@
+# Generated with:
+development/tools/make_key cts-net-app '/CN=cts-net-app'
diff --git a/tests/cts/hostside/certs/cts-net-app.pk8 b/tests/cts/hostside/certs/cts-net-app.pk8
new file mode 100644
index 0000000..1703e4e
--- /dev/null
+++ b/tests/cts/hostside/certs/cts-net-app.pk8
Binary files differ
diff --git a/tests/cts/hostside/certs/cts-net-app.x509.pem b/tests/cts/hostside/certs/cts-net-app.x509.pem
new file mode 100644
index 0000000..a15ff48
--- /dev/null
+++ b/tests/cts/hostside/certs/cts-net-app.x509.pem
@@ -0,0 +1,19 @@
+-----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/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
new file mode 100644
index 0000000..1312085
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -0,0 +1,42 @@
+/*
+ * 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.net;
+public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ public void testOnBlockedStatusChanged_dataSaver() throws Exception {
+ runDeviceTests(TEST_PKG,
+ TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
+ }
+
+ public void testOnBlockedStatusChanged_powerSaver() throws Exception {
+ runDeviceTests(TEST_PKG,
+ TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
+ }
+}
+
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
new file mode 100644
index 0000000..89c79d3
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -0,0 +1,188 @@
+/*
+ * 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.net;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.io.FileNotFoundException;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
+ IBuildReceiver {
+ protected static final boolean DEBUG = false;
+ protected static final String TAG = "HostsideNetworkTests";
+ protected static final String TEST_PKG = "com.android.cts.net.hostside";
+ protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
+ protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+ protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
+
+ private IAbi mAbi;
+ private IBuildInfo mCtsBuild;
+
+ @Override
+ public void setAbi(IAbi abi) {
+ mAbi = abi;
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mCtsBuild = buildInfo;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ assertNotNull(mAbi);
+ assertNotNull(mCtsBuild);
+
+ uninstallPackage(TEST_PKG, false);
+ installPackage(TEST_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_PKG, true);
+ }
+
+ protected void installPackage(String apk) throws FileNotFoundException,
+ DeviceNotAvailableException {
+ CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+ assertNull(getDevice().installPackage(buildHelper.getTestFile(apk),
+ false /* reinstall */, true /* grantPermissions */, "-t"));
+ }
+
+ protected void uninstallPackage(String packageName, boolean shouldSucceed)
+ throws DeviceNotAvailableException {
+ final String result = getDevice().uninstallPackage(packageName);
+ if (shouldSucceed) {
+ assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
+ }
+ }
+
+ protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException,
+ InterruptedException {
+ 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.net.hostside' could return
+ // 'com.android.cts.net.hostside.app2'
+ boolean found = false;
+ for (String line : result.split("[\\r\\n]+")) {
+ if (line.endsWith(packageName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return;
+ }
+ i++;
+ Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
+ + "); sleeping 1s before polling again");
+ Thread.sleep(1000);
+ }
+ fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
+ }
+
+ protected void runDeviceTests(String packageName, String testClassName)
+ throws DeviceNotAvailableException {
+ runDeviceTests(packageName, testClassName, null);
+ }
+
+ protected void runDeviceTests(String packageName, String testClassName, String methodName)
+ throws DeviceNotAvailableException {
+ RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
+ "androidx.test.runner.AndroidJUnitRunner", getDevice().getIDevice());
+
+ if (testClassName != null) {
+ if (methodName != null) {
+ testRunner.setMethodName(testClassName, methodName);
+ } else {
+ testRunner.setClassName(testClassName);
+ }
+ }
+
+ final CollectingTestListener listener = new CollectingTestListener();
+ getDevice().runInstrumentationTests(testRunner, listener);
+
+ final TestRunResult result = listener.getCurrentRunResults();
+ if (result.isRunFailure()) {
+ throw new AssertionError("Failed to successfully run device tests for "
+ + result.getName() + ": " + result.getRunFailureMessage());
+ }
+
+ if (result.hasFailedTests()) {
+ // build a meaningful error message
+ StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
+ for (Map.Entry<TestDescription, TestResult> resultEntry :
+ result.getTestResults().entrySet()) {
+ final TestStatus testStatus = resultEntry.getValue().getStatus();
+ if (!TestStatus.PASSED.equals(testStatus)
+ && !TestStatus.ASSUMPTION_FAILURE.equals(testStatus)) {
+ errorBuilder.append(resultEntry.getKey().toString());
+ errorBuilder.append(":\n");
+ errorBuilder.append(resultEntry.getValue().getStackTrace());
+ }
+ }
+ throw new AssertionError(errorBuilder.toString());
+ }
+ }
+
+ private static final Pattern UID_PATTERN =
+ Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE);
+
+ protected int getUid(String packageName) throws DeviceNotAvailableException {
+ final String output = runCommand("dumpsys package " + packageName);
+ final Matcher matcher = UID_PATTERN.matcher(output);
+ while (matcher.find()) {
+ final String match = matcher.group(1);
+ return Integer.parseInt(match);
+ }
+ throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n"
+ + output);
+ }
+
+ 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/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
new file mode 100644
index 0000000..d026fe0
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -0,0 +1,401 @@
+/*
+ * 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.net;
+
+import android.platform.test.annotations.FlakyTest;
+
+import com.android.ddmlib.Log;
+import com.android.tradefed.device.DeviceNotAvailableException;
+
+public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkTestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ /**************************
+ * Data Saver Mode tests. *
+ **************************/
+
+ public void testDataSaverMode_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_disabled");
+ }
+
+ public void testDataSaverMode_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_whitelisted");
+ }
+
+ public void testDataSaverMode_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_enabled");
+ }
+
+ public void testDataSaverMode_blacklisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_blacklisted");
+ }
+
+ 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);
+ }
+
+ public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
+ }
+
+ public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testBroadcastNotSentOnUnsupportedDevices");
+ }
+
+ /*****************************
+ * Battery Saver Mode tests. *
+ *****************************/
+
+ public void testBatterySaverModeMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testBatterySaverModeMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testBatterySaverModeMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ 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);
+ }
+
+ public void testBatterySaverModeNonMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testBatterySaverModeNonMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ /*******************
+ * App idle tests. *
+ *******************/
+
+ public void testAppIdleMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ @FlakyTest(bugId=170180675)
+ public void testAppIdleMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testAppIdleMetered_tempWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_tempWhitelisted");
+ }
+
+ public void testAppIdleMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testAppIdleMetered_idleWhitelisted() throws Exception {
+ runDeviceTests(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 {
+ // }
+
+ public void testAppIdleNonMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ @FlakyTest(bugId=170180675)
+ public void testAppIdleNonMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_tempWhitelisted");
+ }
+
+ public void testAppIdleNonMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testAppIdleNetworkAccess_idleWhitelisted");
+ }
+
+ public void testAppIdleNonMetered_whenCharging() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testAppIdleNetworkAccess_whenCharging");
+ }
+
+ public void testAppIdleMetered_whenCharging() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testAppIdleNetworkAccess_whenCharging");
+ }
+
+ public void testAppIdle_toast() throws Exception {
+ // Check that showing a toast doesn't bring an app out of standby
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testAppIdle_toast");
+ }
+
+ /********************
+ * Doze Mode tests. *
+ ********************/
+
+ public void testDozeModeMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testDozeModeMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testDozeModeMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
+ runDeviceTests(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 {
+ // }
+
+ public void testDozeModeNonMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testDozeModeNonMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testDozeModeNonMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
+ throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
+ }
+
+ /**********************
+ * Mixed modes tests. *
+ **********************/
+
+ public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDataAndBatterySaverModes_meteredNetwork");
+ }
+
+ public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDataAndBatterySaverModes_nonMeteredNetwork");
+ }
+
+ public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDozeAndBatterySaverMode_powerSaveWhitelists");
+ }
+
+ public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDozeAndAppIdle_powerSaveWhitelists");
+ }
+
+ public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndDoze_tempPowerSaveWhitelists");
+ }
+
+ public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
+ }
+
+ public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDozeAndAppIdle_appIdleWhitelist");
+ }
+
+ public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
+ }
+
+ public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
+ }
+
+ /**************************
+ * Restricted mode tests. *
+ **************************/
+ public void testNetworkAccess_restrictedMode() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+ "testNetworkAccess");
+ }
+
+ /************************
+ * Expedited job tests. *
+ ************************/
+
+ public void testMeteredNetworkAccess_expeditedJob() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
+ }
+
+ public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
+ runDeviceTests(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");
+ Thread.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");
+ Thread.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/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
new file mode 100644
index 0000000..49b5f9d
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -0,0 +1,103 @@
+/*
+ * 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.net;
+
+public class HostsideVpnTests extends HostsideNetworkTestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ public void testDefault() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault");
+ }
+
+ public void testAppAllowed() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppAllowed");
+ }
+
+ public void testAppDisallowed() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppDisallowed");
+ }
+
+ public void testGetConnectionOwnerUidSecurity() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testGetConnectionOwnerUidSecurity");
+ }
+
+ public void testSetProxy() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxy");
+ }
+
+ public void testSetProxyDisallowedApps() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxyDisallowedApps");
+ }
+
+ public void testNoProxy() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testNoProxy");
+ }
+
+ public void testBindToNetworkWithProxy() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBindToNetworkWithProxy");
+ }
+
+ public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNoUnderlyingNetwork");
+ }
+
+ public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNullUnderlyingNetwork");
+ }
+
+ public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNonNullUnderlyingNetwork");
+ }
+
+ public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testAlwaysMeteredVpnWithNullUnderlyingNetwork");
+ }
+
+ public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG,
+ TEST_PKG + ".VpnTest",
+ "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork");
+ }
+
+ public void testB141603906() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testB141603906");
+ }
+
+ public void testDownloadWithDownloadManagerDisallowed() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+ "testDownloadWithDownloadManagerDisallowed");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
new file mode 100644
index 0000000..23aca24
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
@@ -0,0 +1,92 @@
+/*
+ * 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.net;
+
+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/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
new file mode 100644
index 0000000..19e61c6
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.security.cts;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.testtype.IDeviceTest;
+
+import java.lang.Integer;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Host-side tests for values in /proc/net.
+ *
+ * These tests analyze /proc/net to verify that certain networking properties are correct.
+ */
+public class ProcNetTest extends DeviceTestCase implements IBuildReceiver, IDeviceTest {
+ private static final String SPI_TIMEOUT_SYSCTL = "/proc/sys/net/core/xfrm_acq_expires";
+ private static final int MIN_ACQ_EXPIRES = 3600;
+ // Global sysctls. Must be present and set to 1.
+ private static final String[] GLOBAL_SYSCTLS = {
+ "/proc/sys/net/ipv4/fwmark_reflect",
+ "/proc/sys/net/ipv6/fwmark_reflect",
+ "/proc/sys/net/ipv4/tcp_fwmark_accept",
+ };
+
+ // Per-interface IPv6 autoconf sysctls.
+ private static final String IPV6_SYSCTL_DIR = "/proc/sys/net/ipv6/conf";
+ private static final String AUTOCONF_SYSCTL = "accept_ra_rt_table";
+
+ // Expected values for MIN|MAX_PLEN.
+ private static final String ACCEPT_RA_RT_INFO_MIN_PLEN_STRING = "accept_ra_rt_info_min_plen";
+ private static final int ACCEPT_RA_RT_INFO_MIN_PLEN_VALUE = 48;
+ private static final String ACCEPT_RA_RT_INFO_MAX_PLEN_STRING = "accept_ra_rt_info_max_plen";
+ private static final int ACCEPT_RA_RT_INFO_MAX_PLEN_VALUE = 64;
+ // Expected values for RFC 7559 router soliciations.
+ // Maximum number of router solicitations to send. -1 means no limit.
+ private static final int IPV6_WIFI_ROUTER_SOLICITATIONS = -1;
+ private ITestDevice mDevice;
+ private IBuildInfo mBuild;
+ private String[] mSysctlDirs;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setBuild(IBuildInfo build) {
+ mBuild = build;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDevice(ITestDevice device) {
+ super.setDevice(device);
+ mDevice = device;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mSysctlDirs = getSysctlDirs();
+ }
+
+ private String[] getSysctlDirs() throws Exception {
+ String interfaceDirs[] = mDevice.executeAdbCommand("shell", "ls", "-1",
+ IPV6_SYSCTL_DIR).split("\n");
+ List<String> interfaceDirsList = new ArrayList<String>(Arrays.asList(interfaceDirs));
+ interfaceDirsList.remove("all");
+ interfaceDirsList.remove("lo");
+ return interfaceDirsList.toArray(new String[interfaceDirsList.size()]);
+ }
+
+
+ protected void assertLess(String sysctl, int a, int b) {
+ assertTrue("value of " + sysctl + ": expected < " + b + " but was: " + a, a < b);
+ }
+
+ protected void assertAtLeast(String sysctl, int a, int b) {
+ assertTrue("value of " + sysctl + ": expected >= " + b + " but was: " + a, a >= b);
+ }
+
+ public int readIntFromPath(String path) throws Exception {
+ String mode = mDevice.executeAdbCommand("shell", "stat", "-c", "%a", path).trim();
+ String user = mDevice.executeAdbCommand("shell", "stat", "-c", "%u", path).trim();
+ String group = mDevice.executeAdbCommand("shell", "stat", "-c", "%g", path).trim();
+ assertEquals(mode, "644");
+ assertEquals(user, "0");
+ assertEquals(group, "0");
+ return Integer.parseInt(mDevice.executeAdbCommand("shell", "cat", path).trim());
+ }
+
+ /**
+ * Checks that SPI default timeouts are overridden, and set to a reasonable length of time
+ */
+ public void testMinAcqExpires() throws Exception {
+ int value = readIntFromPath(SPI_TIMEOUT_SYSCTL);
+ assertAtLeast(SPI_TIMEOUT_SYSCTL, value, MIN_ACQ_EXPIRES);
+ }
+
+ /**
+ * Checks that the sysctls for multinetwork kernel features are present and
+ * enabled.
+ */
+ public void testProcSysctls() throws Exception {
+ for (String sysctl : GLOBAL_SYSCTLS) {
+ int value = readIntFromPath(sysctl);
+ assertEquals(sysctl, 1, value);
+ }
+
+ for (String interfaceDir : mSysctlDirs) {
+ String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + AUTOCONF_SYSCTL;
+ int value = readIntFromPath(path);
+ assertLess(path, value, 0);
+ }
+ }
+
+ /**
+ * Verify that accept_ra_rt_info_{min,max}_plen exists and is set to the expected value
+ */
+ public void testAcceptRaRtInfoMinMaxPlen() throws Exception {
+ for (String interfaceDir : mSysctlDirs) {
+ String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_min_plen";
+ int value = readIntFromPath(path);
+ assertEquals(path, value, ACCEPT_RA_RT_INFO_MIN_PLEN_VALUE);
+ path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_max_plen";
+ value = readIntFromPath(path);
+ assertEquals(path, value, ACCEPT_RA_RT_INFO_MAX_PLEN_VALUE);
+ }
+ }
+
+ /**
+ * Verify that router_solicitations exists and is set to the expected value
+ * and verify that router_solicitation_max_interval exists and is in an acceptable interval.
+ */
+ public void testRouterSolicitations() throws Exception {
+ for (String interfaceDir : mSysctlDirs) {
+ String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations";
+ int value = readIntFromPath(path);
+ assertEquals(IPV6_WIFI_ROUTER_SOLICITATIONS, value);
+ path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitation_max_interval";
+ int interval = readIntFromPath(path);
+ final int lowerBoundSec = 15 * 60;
+ final int upperBoundSec = 60 * 60;
+ assertTrue(lowerBoundSec <= interval);
+ assertTrue(interval <= upperBoundSec);
+ }
+ }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
new file mode 100644
index 0000000..25596a9
--- /dev/null
+++ b/tests/cts/net/Android.bp
@@ -0,0 +1,103 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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"],
+}
+
+java_defaults {
+ name: "CtsNetTestCasesDefaults",
+ defaults: [
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+
+ libs: [
+ "voip-common",
+ "android.test.base",
+ ],
+
+ jni_libs: [
+ "libcts_jni",
+ "libnativedns_jni",
+ "libnativemultinetwork_jni",
+ "libnativehelper_compat_libc++",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ jarjar_rules: "jarjar-rules-shared.txt",
+ static_libs: [
+ "bouncycastle-unbundled",
+ "FrameworksNetCommonTests",
+ "core-tests-support",
+ "cts-net-utils",
+ "ctstestrunner-axt",
+ "junit",
+ "junit-params",
+ "modules-utils-build",
+ "net-utils-framework-common",
+ "truth-prebuilt",
+ ],
+
+ // uncomment when b/13249961 is fixed
+ // sdk_version: "current",
+ platform_apis: true,
+}
+
+// Networking CTS tests for development and release. These tests always target the platform SDK
+// version, and are subject to all the restrictions appropriate to that version. Before SDK
+// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
+// devices.
+android_test {
+ name: "CtsNetTestCases",
+ defaults: ["CtsNetTestCasesDefaults"],
+ // TODO: CTS should not depend on the entirety of the networkstack code.
+ static_libs: [
+ "NetworkStackApiCurrentLib",
+ ],
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ test_config_template: "AndroidTestTemplate.xml",
+}
+
+// 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: ["CtsNetTestCasesDefaults"],
+ // TODO: CTS should not depend on the entirety of the networkstack code.
+ static_libs: [
+ "NetworkStackApiStableLib",
+ ],
+ jni_uses_sdk_apis: true,
+ min_sdk_version: "29",
+ target_sdk_version: "30",
+ test_suites: [
+ "general-tests",
+ "mts-dnsresolver",
+ "mts-networking",
+ "mts-tethering",
+ "mts-wifi",
+ ],
+ test_config_template: "AndroidTestTemplate.xml",
+}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
new file mode 100644
index 0000000..3b47100
--- /dev/null
+++ b/tests/cts/net/AndroidManifest.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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="android.net.cts"
+ android:targetSandboxVersion="2">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <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.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
+ <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
+
+ <!-- TODO (b/186093901): remove after fixing resource querying -->
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+ <!-- This test also uses signature permissions through adopting the shell identity.
+ The permissions acquired that way include (probably not exhaustive) :
+ android.permission.MANAGE_TEST_NETWORKS
+ -->
+
+ <application android:usesCleartextTraffic="true">
+ <uses-library android:name="android.test.runner" />
+ <uses-library android:name="org.apache.http.legacy" android:required="false" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.cts"
+ android:label="CTS tests of android.net">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener" />
+ </instrumentation>
+
+</manifest>
+
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
new file mode 100644
index 0000000..474eefe
--- /dev/null
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -0,0 +1,36 @@
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="Test config for {MODULE}">
+ <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" />
+
+ <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+com.google.android.resolv.apex" />
+ <option name="not-shardable" value="true" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="{MODULE}.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.cts" />
+ <option name="runtime-hint" value="9m4s" />
+ <option name="hidden-api-checks" value="false" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/OWNERS b/tests/cts/net/OWNERS
new file mode 100644
index 0000000..432bd9b
--- /dev/null
+++ b/tests/cts/net/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 31808
+# Inherits parent owners
+per-file src/android/net/cts/NetworkWatchlistTest.java=alanstokes@google.com
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
new file mode 100644
index 0000000..5b37294
--- /dev/null
+++ b/tests/cts/net/api23Test/Android.bp
@@ -0,0 +1,55 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsNetApi23TestCases",
+ defaults: ["cts_defaults"],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+
+ libs: [
+ "android.test.base",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "core-tests-support",
+ "compatibility-device-util-axt",
+ "cts-net-utils",
+ "ctstestrunner-axt",
+ "ctstestserver",
+ "mockwebserver",
+ "junit",
+ "junit-params",
+ "truth-prebuilt",
+ ],
+
+ platform_apis: true,
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+}
diff --git a/tests/cts/net/api23Test/AndroidManifest.xml b/tests/cts/net/api23Test/AndroidManifest.xml
new file mode 100644
index 0000000..69ee0dd
--- /dev/null
+++ b/tests/cts/net/api23Test/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.cts.api23test">
+
+ <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.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application android:usesCleartextTraffic="true">
+ <uses-library android:name="android.test.runner"/>
+
+ <receiver android:name=".ConnectivityReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
+ </intent-filter>
+ </receiver>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.cts.api23test"
+ android:label="CTS tests of android.net">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener"/>
+ </instrumentation>
+</manifest>
diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml
new file mode 100644
index 0000000..8042d50
--- /dev/null
+++ b/tests/cts/net/api23Test/AndroidTest.xml
@@ -0,0 +1,31 @@
+<!-- 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.
+-->
+<configuration description="Config for CTS Net API23 test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <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="not-shardable" value="true" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsNetApi23TestCases.apk" />
+ <option name="test-file-name" value="CtsNetTestAppForApi23.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.cts.api23test" />
+ <option name="hidden-api-checks" value="false" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
new file mode 100644
index 0000000..cdb66e3
--- /dev/null
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.net.cts.api23test;
+
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.cts.util.CtsNetUtils;
+import android.os.Looper;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectivityManagerApi23Test extends AndroidTestCase {
+ private static final String TAG = ConnectivityManagerApi23Test.class.getSimpleName();
+ private static final int SEND_BROADCAST_TIMEOUT = 30000;
+ // Intent string to get the number of wifi CONNECTIVITY_ACTION callbacks the test app has seen
+ public static final String GET_WIFI_CONNECTIVITY_ACTION_COUNT =
+ "android.net.cts.appForApi23.getWifiConnectivityActionCount";
+ // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent.
+
+ private Context mContext;
+ private PackageManager mPackageManager;
+ private CtsNetUtils mCtsNetUtils;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ Looper.prepare();
+ mContext = getContext();
+ mPackageManager = mContext.getPackageManager();
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ }
+
+ /**
+ * Tests reporting of connectivity changed.
+ */
+ public void testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent cannot execute unless device supports WiFi");
+ return;
+ }
+ ConnectivityReceiver.prepare();
+
+ mCtsNetUtils.toggleWifi();
+
+ // The connectivity broadcast has been sent; push through a terminal broadcast
+ // to wait for in the receive to confirm it didn't see the connectivity change.
+ Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
+ finalIntent.setClass(mContext, ConnectivityReceiver.class);
+ mContext.sendBroadcast(finalIntent);
+ assertFalse(ConnectivityReceiver.waitForBroadcast());
+ }
+
+ public void testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent()
+ throws InterruptedException {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent cannot"
+ + "execute unless device supports WiFi");
+ return;
+ }
+ mContext.startActivity(new Intent()
+ .setComponent(new ComponentName("android.net.cts.appForApi23",
+ "android.net.cts.appForApi23.ConnectivityListeningActivity"))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ Thread.sleep(200);
+
+ mCtsNetUtils.toggleWifi();
+
+ Intent getConnectivityCount = new Intent(GET_WIFI_CONNECTIVITY_ACTION_COUNT);
+ assertEquals(2, sendOrderedBroadcastAndReturnResultCode(
+ getConnectivityCount, SEND_BROADCAST_TIMEOUT));
+ }
+
+ public void testConnectivityChanged_whenRegistered_shouldReceiveIntent() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testConnectivityChanged_whenRegistered_shouldReceiveIntent cannot execute unless device supports WiFi");
+ return;
+ }
+ ConnectivityReceiver.prepare();
+ ConnectivityReceiver receiver = new ConnectivityReceiver();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(receiver, filter);
+
+ mCtsNetUtils.toggleWifi();
+ Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
+ finalIntent.setClass(mContext, ConnectivityReceiver.class);
+ mContext.sendBroadcast(finalIntent);
+
+ assertTrue(ConnectivityReceiver.waitForBroadcast());
+ }
+
+ private int sendOrderedBroadcastAndReturnResultCode(
+ Intent intent, int timeoutMs) throws InterruptedException {
+ final LinkedBlockingQueue<Integer> result = new LinkedBlockingQueue<>(1);
+ mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ result.offer(getResultCode());
+ }
+ }, null, 0, null, null);
+
+ Integer resultCode = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ assertNotNull("Timed out (more than " + timeoutMs +
+ " milliseconds) waiting for result code for broadcast", resultCode);
+ return resultCode;
+ }
+
+}
\ No newline at end of file
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java
new file mode 100644
index 0000000..9d2b8ad
--- /dev/null
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java
@@ -0,0 +1,69 @@
+/*
+ * 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 android.net.cts.api23test;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectivityReceiver extends BroadcastReceiver {
+ static boolean sReceivedConnectivity;
+ static boolean sReceivedFinal;
+ static CountDownLatch sLatch;
+
+ static void prepare() {
+ synchronized (ConnectivityReceiver.class) {
+ sReceivedConnectivity = sReceivedFinal = false;
+ sLatch = new CountDownLatch(1);
+ }
+ }
+
+ static boolean waitForBroadcast() {
+ try {
+ sLatch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ synchronized (ConnectivityReceiver.class) {
+ sLatch = null;
+ if (!sReceivedFinal) {
+ throw new IllegalStateException("Never received final broadcast");
+ }
+ return sReceivedConnectivity;
+ }
+ }
+
+ static final String FINAL_ACTION = "android.net.cts.action.FINAL";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i("ConnectivityReceiver", "Received: " + intent.getAction());
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ sReceivedConnectivity = true;
+ } else if (FINAL_ACTION.equals(intent.getAction())) {
+ sReceivedFinal = true;
+ if (sLatch != null) {
+ sLatch.countDown();
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
new file mode 100644
index 0000000..b39690f
--- /dev/null
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -0,0 +1,36 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsNetTestAppForApi23",
+ defaults: ["cts_defaults"],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+
+ srcs: ["src/**/*.java"],
+
+ sdk_version: "23",
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+}
diff --git a/tests/cts/net/appForApi23/AndroidManifest.xml b/tests/cts/net/appForApi23/AndroidManifest.xml
new file mode 100644
index 0000000..158b9c4
--- /dev/null
+++ b/tests/cts/net/appForApi23/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.cts.appForApi23">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application>
+ <receiver android:name=".ConnectivityReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.cts.appForApi23.getWifiConnectivityActionCount"/>
+ </intent-filter>
+ </receiver>
+
+ <activity android:name=".ConnectivityListeningActivity"
+ android:label="ConnectivityListeningActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java
new file mode 100644
index 0000000..24fb68e8
--- /dev/null
+++ b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java
@@ -0,0 +1,22 @@
+/*
+ * 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 android.net.cts.appForApi23;
+
+import android.app.Activity;
+
+// Stub activity used to start the app
+public class ConnectivityListeningActivity extends Activity {
+}
\ No newline at end of file
diff --git a/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java
new file mode 100644
index 0000000..8039a4f
--- /dev/null
+++ b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.net.cts.appForApi23;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+
+public class ConnectivityReceiver extends BroadcastReceiver {
+ public static String GET_WIFI_CONNECTIVITY_ACTION_COUNT =
+ "android.net.cts.appForApi23.getWifiConnectivityActionCount";
+
+ private static int sWifiConnectivityActionCount = 0;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ int networkType = intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, 0);
+ if (networkType == ConnectivityManager.TYPE_WIFI) {
+ sWifiConnectivityActionCount++;
+ }
+ }
+ if (GET_WIFI_CONNECTIVITY_ACTION_COUNT.equals(intent.getAction())) {
+ setResultCode(sWifiConnectivityActionCount);
+ }
+ }
+}
diff --git a/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml b/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml
new file mode 100644
index 0000000..19628d1
--- /dev/null
+++ b/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright (C) 2018 The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<!-- This test config file is for NetworkWatchlistTest tests -->
+<watchlist-config>
+ <sha256-domain>
+ </sha256-domain>
+ <sha256-ip>
+ </sha256-ip>
+ <crc32-domain>
+ </crc32-domain>
+ <crc32-ip>
+ </crc32-ip>
+</watchlist-config>
diff --git a/tests/cts/net/assets/network_watchlist_config_for_test.xml b/tests/cts/net/assets/network_watchlist_config_for_test.xml
new file mode 100644
index 0000000..835ae0f
--- /dev/null
+++ b/tests/cts/net/assets/network_watchlist_config_for_test.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright (C) 2018 The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<!-- This test config file just contains some random hashes for testing
+ConnectivityManager.getWatchlistConfigHash() -->
+<watchlist-config>
+ <sha256-domain>
+ <hash>F0905DA7549614957B449034C281EF7BDEFDBC2B6E050AD1E78D6DE18FBD0D5F</hash>
+ </sha256-domain>
+ <sha256-ip>
+ <hash>18DD41C9F2E8E4879A1575FB780514EF33CF6E1F66578C4AE7CCA31F49B9F2EC</hash>
+ </sha256-ip>
+ <crc32-domain>
+ <hash>AAAAAAAA</hash>
+ </crc32-domain>
+ <crc32-ip>
+ <hash>BBBBBBBB</hash>
+ </crc32-ip>
+</watchlist-config>
diff --git a/tests/cts/net/jarjar-rules-shared.txt b/tests/cts/net/jarjar-rules-shared.txt
new file mode 100644
index 0000000..11dba74
--- /dev/null
+++ b/tests/cts/net/jarjar-rules-shared.txt
@@ -0,0 +1,2 @@
+# Module library in frameworks/libs/net
+rule com.android.net.module.util.** android.net.cts.util.@1
\ No newline at end of file
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
new file mode 100644
index 0000000..13f38d7
--- /dev/null
+++ b/tests/cts/net/jni/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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"],
+}
+
+cc_library_shared {
+ name: "libnativedns_jni",
+
+ srcs: ["NativeDnsJni.c"],
+ sdk_version: "current",
+
+ shared_libs: [
+ "libnativehelper_compat_libc++",
+ "liblog",
+ ],
+ stl: "libc++_static",
+
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+
+}
+
+cc_library_shared {
+ name: "libnativemultinetwork_jni",
+
+ srcs: ["NativeMultinetworkJni.cpp"],
+ sdk_version: "current",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-format",
+ ],
+ shared_libs: [
+ "libandroid",
+ "libnativehelper_compat_libc++",
+ "liblog",
+ ],
+ stl: "libc++_static",
+}
diff --git a/tests/cts/net/jni/NativeDnsJni.c b/tests/cts/net/jni/NativeDnsJni.c
new file mode 100644
index 0000000..4ec800e
--- /dev/null
+++ b/tests/cts/net/jni/NativeDnsJni.c
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <arpa/inet.h>
+#include <jni.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <android/log.h>
+
+#define LOG_TAG "NativeDns-JNI"
+#define LOGD(fmt, ...) \
+ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##__VA_ARGS__)
+
+const char *GoogleDNSIpV4Address="8.8.8.8";
+const char *GoogleDNSIpV4Address2="8.8.4.4";
+const char *GoogleDNSIpV6Address="2001:4860:4860::8888";
+const char *GoogleDNSIpV6Address2="2001:4860:4860::8844";
+
+JNIEXPORT jboolean Java_android_net_cts_DnsTest_testNativeDns(JNIEnv* env, jclass class)
+{
+ const char *node = "www.google.com";
+ char *service = NULL;
+ struct addrinfo *answer;
+
+ int res = getaddrinfo(node, service, NULL, &answer);
+ LOGD("getaddrinfo(www.google.com) gave res=%d (%s)", res, gai_strerror(res));
+ if (res != 0) return JNI_FALSE;
+
+ // check for v4 & v6
+ {
+ int foundv4 = 0;
+ int foundv6 = 0;
+ struct addrinfo *current = answer;
+ while (current != NULL) {
+ char buf[256];
+ if (current->ai_addr->sa_family == AF_INET) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in *)current->ai_addr)->sin_addr,
+ buf, sizeof(buf));
+ foundv4 = 1;
+ LOGD(" %s", buf);
+ } else if (current->ai_addr->sa_family == AF_INET6) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in6 *)current->ai_addr)->sin6_addr,
+ buf, sizeof(buf));
+ foundv6 = 1;
+ LOGD(" %s", buf);
+ }
+ current = current->ai_next;
+ }
+
+ freeaddrinfo(answer);
+ answer = NULL;
+ if (foundv4 != 1 && foundv6 != 1) {
+ LOGD("getaddrinfo(www.google.com) didn't find either v4 or v6 address");
+ return JNI_FALSE;
+ }
+ }
+
+ node = "ipv6.google.com";
+ res = getaddrinfo(node, service, NULL, &answer);
+ LOGD("getaddrinfo(ipv6.google.com) gave res=%d", res);
+ if (res != 0) return JNI_FALSE;
+
+ {
+ int foundv4 = 0;
+ int foundv6 = 0;
+ struct addrinfo *current = answer;
+ while (current != NULL) {
+ char buf[256];
+ if (current->ai_addr->sa_family == AF_INET) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in *)current->ai_addr)->sin_addr,
+ buf, sizeof(buf));
+ LOGD(" %s", buf);
+ foundv4 = 1;
+ } else if (current->ai_addr->sa_family == AF_INET6) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in6 *)current->ai_addr)->sin6_addr,
+ buf, sizeof(buf));
+ LOGD(" %s", buf);
+ foundv6 = 1;
+ }
+ current = current->ai_next;
+ }
+
+ freeaddrinfo(answer);
+ answer = NULL;
+ if (foundv4 == 1 || foundv6 != 1) {
+ LOGD("getaddrinfo(ipv6.google.com) didn't find only v6");
+ return JNI_FALSE;
+ }
+ }
+
+ // getnameinfo
+ struct sockaddr_in sa4;
+ sa4.sin_family = AF_INET;
+ sa4.sin_port = 0;
+ inet_pton(AF_INET, GoogleDNSIpV4Address, &(sa4.sin_addr));
+
+ struct sockaddr_in6 sa6;
+ sa6.sin6_family = AF_INET6;
+ sa6.sin6_port = 0;
+ sa6.sin6_flowinfo = 0;
+ sa6.sin6_scope_id = 0;
+ inet_pton(AF_INET6, GoogleDNSIpV6Address2, &(sa6.sin6_addr));
+
+ char buf[NI_MAXHOST];
+ int flags = NI_NAMEREQD;
+
+ res = getnameinfo((const struct sockaddr*)&sa4, sizeof(sa4), buf, sizeof(buf), NULL, 0, flags);
+ if (res != 0) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) gave error %d (%s)", GoogleDNSIpV4Address, res,
+ gai_strerror(res));
+ return JNI_FALSE;
+ }
+ if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s",
+ GoogleDNSIpV4Address, buf);
+ return JNI_FALSE;
+ }
+
+ memset(buf, 0, sizeof(buf));
+ res = getnameinfo((const struct sockaddr*)&sa6, sizeof(sa6), buf, sizeof(buf), NULL, 0, flags);
+ if (res != 0) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) gave error %d (%s)", GoogleDNSIpV6Address2,
+ res, gai_strerror(res));
+ return JNI_FALSE;
+ }
+ if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s",
+ GoogleDNSIpV6Address2, buf);
+ return JNI_FALSE;
+ }
+
+ // gethostbyname
+ struct hostent *my_hostent = gethostbyname("www.youtube.com");
+ if (my_hostent == NULL) {
+ LOGD("gethostbyname(www.youtube.com) gave null response");
+ return JNI_FALSE;
+ }
+ if ((my_hostent->h_addr_list == NULL) || (*my_hostent->h_addr_list == NULL)) {
+ LOGD("gethostbyname(www.youtube.com) gave 0 addresses");
+ return JNI_FALSE;
+ }
+ {
+ char **current = my_hostent->h_addr_list;
+ while (*current != NULL) {
+ char buf[256];
+ inet_ntop(my_hostent->h_addrtype, *current, buf, sizeof(buf));
+ LOGD("gethostbyname(www.youtube.com) gave %s", buf);
+ current++;
+ }
+ }
+
+ // gethostbyaddr
+ char addr6[16];
+ inet_pton(AF_INET6, GoogleDNSIpV6Address, addr6);
+ my_hostent = gethostbyaddr(addr6, sizeof(addr6), AF_INET6);
+ if (my_hostent == NULL) {
+ LOGD("gethostbyaddr(%s (GoogleDNS) ) gave null response", GoogleDNSIpV6Address);
+ return JNI_FALSE;
+ }
+
+ LOGD("gethostbyaddr(%s (GoogleDNS) ) gave %s for name", GoogleDNSIpV6Address,
+ my_hostent->h_name ? my_hostent->h_name : "null");
+
+ if (my_hostent->h_name == NULL) return JNI_FALSE;
+ return JNI_TRUE;
+}
diff --git a/tests/cts/net/jni/NativeMultinetworkJni.cpp b/tests/cts/net/jni/NativeMultinetworkJni.cpp
new file mode 100644
index 0000000..60e31bc
--- /dev/null
+++ b/tests/cts/net/jni/NativeMultinetworkJni.cpp
@@ -0,0 +1,515 @@
+/*
+ * 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.
+ */
+
+
+#define LOG_TAG "MultinetworkApiTest"
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <jni.h>
+#include <netdb.h>
+#include <poll.h> /* poll */
+#include <resolv.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+
+#include <string>
+
+#include <android/log.h>
+#include <android/multinetwork.h>
+#include <nativehelper/JNIHelp.h>
+
+#define LOGD(fmt, ...) \
+ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##__VA_ARGS__)
+
+#define EXPECT_GE(env, actual, expected, msg) \
+ do { \
+ if (actual < expected) { \
+ jniThrowExceptionFmt(env, "java/lang/AssertionError", \
+ "%s:%d: %s EXPECT_GE: expected %d, got %d", \
+ __FILE__, __LINE__, msg, expected, actual); \
+ } \
+ } while (0)
+
+#define EXPECT_GT(env, actual, expected, msg) \
+ do { \
+ if (actual <= expected) { \
+ jniThrowExceptionFmt(env, "java/lang/AssertionError", \
+ "%s:%d: %s EXPECT_GT: expected %d, got %d", \
+ __FILE__, __LINE__, msg, expected, actual); \
+ } \
+ } while (0)
+
+#define EXPECT_EQ(env, expected, actual, msg) \
+ do { \
+ if (actual != expected) { \
+ jniThrowExceptionFmt(env, "java/lang/AssertionError", \
+ "%s:%d: %s EXPECT_EQ: expected %d, got %d", \
+ __FILE__, __LINE__, msg, expected, actual); \
+ } \
+ } while (0)
+
+static const int MAXPACKET = 8 * 1024;
+static const int TIMEOUT_MS = 15000;
+static const char kHostname[] = "connectivitycheck.android.com";
+static const char kNxDomainName[] = "test1-nx.metric.gstatic.com";
+static const char kGoogleName[] = "www.google.com";
+
+int makeQuery(const char* name, int qtype, uint8_t* buf, size_t buflen) {
+ return res_mkquery(ns_o_query, name, ns_c_in, qtype, NULL, 0, NULL, buf, buflen);
+}
+
+int getAsyncResponse(JNIEnv* env, int fd, int timeoutMs, int* rcode, uint8_t* buf, size_t bufLen) {
+ struct pollfd wait_fd = { .fd = fd, .events = POLLIN };
+
+ poll(&wait_fd, 1, timeoutMs);
+ if (wait_fd.revents & POLLIN) {
+ int n = android_res_nresult(fd, rcode, buf, bufLen);
+ // Verify that android_res_nresult() closed the fd
+ char dummy;
+ EXPECT_EQ(env, -1, read(fd, &dummy, sizeof(dummy)), "res_nresult check for closing fd");
+ EXPECT_EQ(env, EBADF, errno, "res_nresult check for errno");
+ return n;
+ }
+
+ return -ETIMEDOUT;
+}
+
+int extractIpAddressAnswers(uint8_t* buf, size_t bufLen, int family) {
+ ns_msg handle;
+ if (ns_initparse((const uint8_t*) buf, bufLen, &handle) < 0) {
+ return -errno;
+ }
+ const int ancount = ns_msg_count(handle, ns_s_an);
+ // Answer count = 0 is valid(e.g. response of query with root)
+ if (!ancount) {
+ return 0;
+ }
+ ns_rr rr;
+ bool hasValidAns = false;
+ for (int i = 0; i < ancount; i++) {
+ if (ns_parserr(&handle, ns_s_an, i, &rr) < 0) {
+ // If there is no valid answer, test will fail.
+ continue;
+ }
+ const uint8_t* rdata = ns_rr_rdata(rr);
+ char buffer[INET6_ADDRSTRLEN];
+ if (inet_ntop(family, (const char*) rdata, buffer, sizeof(buffer)) == NULL) {
+ return -errno;
+ }
+ hasValidAns = true;
+ }
+ return hasValidAns ? 0 : -EBADMSG;
+}
+
+int expectAnswersValid(JNIEnv* env, int fd, int family, int expectedRcode) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(env, fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ if (res < 0) {
+ return res;
+ }
+
+ EXPECT_EQ(env, expectedRcode, rcode, "rcode is not expected");
+
+ if (expectedRcode == ns_r_noerror && res > 0) {
+ return extractIpAddressAnswers(buf, res, family);
+ }
+ return 0;
+}
+
+int expectAnswersNotValid(JNIEnv* env, int fd, int expectedErrno) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(env, fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ if (res != expectedErrno) {
+ LOGD("res:%d, expectedErrno = %d", res, expectedErrno);
+ return (res > 0) ? -EREMOTEIO : res;
+ }
+ return 0;
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNqueryCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ // V4
+ int fd = android_res_nquery(handle, kHostname, ns_c_in, ns_t_a, 0);
+ EXPECT_GE(env, fd, 0, "v4 res_nquery");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror),
+ "v4 res_nquery check answers");
+
+ // V6
+ fd = android_res_nquery(handle, kHostname, ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(env, fd, 0, "v6 res_nquery");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror),
+ "v6 res_nquery check answers");
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNsendCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+ // V4
+ uint8_t buf1[MAXPACKET] = {};
+
+ int len1 = makeQuery(kGoogleName, ns_t_a, buf1, sizeof(buf1));
+ EXPECT_GT(env, len1, 0, "v4 res_mkquery 1st");
+
+ uint8_t buf2[MAXPACKET] = {};
+ int len2 = makeQuery(kHostname, ns_t_a, buf2, sizeof(buf2));
+ EXPECT_GT(env, len2, 0, "v4 res_mkquery 2nd");
+
+ int fd1 = android_res_nsend(handle, buf1, len1, 0);
+ EXPECT_GE(env, fd1, 0, "v4 res_nsend 1st");
+ int fd2 = android_res_nsend(handle, buf2, len2, 0);
+ EXPECT_GE(env, fd2, 0, "v4 res_nsend 2nd");
+
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd2, AF_INET, ns_r_noerror),
+ "v4 res_nsend 2nd check answers");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd1, AF_INET, ns_r_noerror),
+ "v4 res_nsend 1st check answers");
+
+ // V6
+ memset(buf1, 0, sizeof(buf1));
+ memset(buf2, 0, sizeof(buf2));
+ len1 = makeQuery(kGoogleName, ns_t_aaaa, buf1, sizeof(buf1));
+ EXPECT_GT(env, len1, 0, "v6 res_mkquery 1st");
+ len2 = makeQuery(kHostname, ns_t_aaaa, buf2, sizeof(buf2));
+ EXPECT_GT(env, len2, 0, "v6 res_mkquery 2nd");
+
+ fd1 = android_res_nsend(handle, buf1, len1, 0);
+ EXPECT_GE(env, fd1, 0, "v6 res_nsend 1st");
+ fd2 = android_res_nsend(handle, buf2, len2, 0);
+ EXPECT_GE(env, fd2, 0, "v6 res_nsend 2nd");
+
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd2, AF_INET6, ns_r_noerror),
+ "v6 res_nsend 2nd check answers");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd1, AF_INET6, ns_r_noerror),
+ "v6 res_nsend 1st check answers");
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNnxDomainCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ // res_nquery V4 NXDOMAIN
+ int fd = android_res_nquery(handle, kNxDomainName, ns_c_in, ns_t_a, 0);
+ EXPECT_GE(env, fd, 0, "v4 res_nquery NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_nxdomain),
+ "v4 res_nquery NXDOMAIN check answers");
+
+ // res_nquery V6 NXDOMAIN
+ fd = android_res_nquery(handle, kNxDomainName, ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(env, fd, 0, "v6 res_nquery NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET6, ns_r_nxdomain),
+ "v6 res_nquery NXDOMAIN check answers");
+
+ uint8_t buf[MAXPACKET] = {};
+ // res_nsend V4 NXDOMAIN
+ int len = makeQuery(kNxDomainName, ns_t_a, buf, sizeof(buf));
+ EXPECT_GT(env, len, 0, "v4 res_mkquery NXDOMAIN");
+ fd = android_res_nsend(handle, buf, len, 0);
+ EXPECT_GE(env, fd, 0, "v4 res_nsend NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_nxdomain),
+ "v4 res_nsend NXDOMAIN check answers");
+
+ // res_nsend V6 NXDOMAIN
+ memset(buf, 0, sizeof(buf));
+ len = makeQuery(kNxDomainName, ns_t_aaaa, buf, sizeof(buf));
+ EXPECT_GT(env, len, 0, "v6 res_mkquery NXDOMAIN");
+ fd = android_res_nsend(handle, buf, len, 0);
+ EXPECT_GE(env, fd, 0, "v6 res_nsend NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET6, ns_r_nxdomain),
+ "v6 res_nsend NXDOMAIN check answers");
+}
+
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNcancelCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ int fd = android_res_nquery(handle, kGoogleName, ns_c_in, ns_t_a, 0);
+ errno = 0;
+ android_res_cancel(fd);
+ int err = errno;
+ EXPECT_EQ(env, 0, err, "res_cancel");
+ // DO NOT call cancel or result with the same fd more than once,
+ // otherwise it will hit fdsan double-close fd.
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNapiMalformedCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ // It is the equivalent of "dig . a", Query with an empty name.
+ int fd = android_res_nquery(handle, "", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(env, fd, 0, "res_nquery root");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror),
+ "res_nquery root check answers");
+
+ // Label limit 63
+ std::string exceedingLabelQuery = "www." + std::string(70, 'g') + ".com";
+ // Name limit 255
+ std::string exceedingDomainQuery = "www." + std::string(255, 'g') + ".com";
+
+ fd = android_res_nquery(handle, exceedingLabelQuery.c_str(), ns_c_in, ns_t_a, 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nquery exceedingLabelQuery");
+ fd = android_res_nquery(handle, exceedingDomainQuery.c_str(), ns_c_in, ns_t_aaaa, 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nquery exceedingDomainQuery");
+
+ uint8_t buf[10] = {};
+ // empty BLOB
+ fd = android_res_nsend(handle, buf, 10, 0);
+ EXPECT_GE(env, fd, 0, "res_nsend empty BLOB");
+ EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL),
+ "res_nsend empty BLOB check answers");
+
+ uint8_t largeBuf[2 * MAXPACKET] = {};
+ // A buffer larger than 8KB
+ fd = android_res_nsend(handle, largeBuf, sizeof(largeBuf), 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend buffer larger than 8KB");
+
+ // 5000 bytes filled with 0. This returns EMSGSIZE because FrameworkListener limits the size of
+ // commands to 4096 bytes.
+ fd = android_res_nsend(handle, largeBuf, 5000, 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend 5000 bytes filled with 0");
+
+ // 500 bytes filled with 0
+ fd = android_res_nsend(handle, largeBuf, 500, 0);
+ EXPECT_GE(env, fd, 0, "res_nsend 500 bytes filled with 0");
+ EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL),
+ "res_nsend 500 bytes filled with 0 check answers");
+
+ // 5000 bytes filled with 0xFF
+ uint8_t ffBuf[5001] = {};
+ memset(ffBuf, 0xFF, sizeof(ffBuf));
+ ffBuf[5000] = '\0';
+ fd = android_res_nsend(handle, ffBuf, sizeof(ffBuf), 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend 5000 bytes filled with 0xFF");
+
+ // 500 bytes filled with 0xFF
+ ffBuf[500] = '\0';
+ fd = android_res_nsend(handle, ffBuf, 501, 0);
+ EXPECT_GE(env, fd, 0, "res_nsend 500 bytes filled with 0xFF");
+ EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL),
+ "res_nsend 500 bytes filled with 0xFF check answers");
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runGetaddrinfoCheck(
+ JNIEnv*, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+ struct addrinfo *res = NULL;
+
+ errno = 0;
+ int rval = android_getaddrinfofornetwork(handle, kHostname, NULL, NULL, &res);
+ const int saved_errno = errno;
+ freeaddrinfo(res);
+
+ LOGD("android_getaddrinfofornetwork(%" PRIu64 ", %s) returned rval=%d errno=%d",
+ handle, kHostname, rval, saved_errno);
+ return rval == 0 ? 0 : -saved_errno;
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runSetprocnetwork(
+ JNIEnv*, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ errno = 0;
+ int rval = android_setprocnetwork(handle);
+ const int saved_errno = errno;
+ LOGD("android_setprocnetwork(%" PRIu64 ") returned rval=%d errno=%d",
+ handle, rval, saved_errno);
+ return rval == 0 ? 0 : -saved_errno;
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runSetsocknetwork(
+ JNIEnv*, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ errno = 0;
+ int fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ if (fd < 0) {
+ LOGD("socket() failed, errno=%d", errno);
+ return -errno;
+ }
+
+ errno = 0;
+ int rval = android_setsocknetwork(handle, fd);
+ const int saved_errno = errno;
+ LOGD("android_setprocnetwork(%" PRIu64 ", %d) returned rval=%d errno=%d",
+ handle, fd, rval, saved_errno);
+ close(fd);
+ return rval == 0 ? 0 : -saved_errno;
+}
+
+// Use sizeof("x") - 1 because we need a compile-time constant, and strlen("x")
+// isn't guaranteed to fold to a constant.
+static const int kSockaddrStrLen = INET6_ADDRSTRLEN + sizeof("[]:65535") - 1;
+
+void sockaddr_ntop(const struct sockaddr *sa, socklen_t salen, char *dst, const size_t size) {
+ char addrstr[INET6_ADDRSTRLEN];
+ char portstr[sizeof("65535")];
+ char buf[kSockaddrStrLen+1];
+
+ int ret = getnameinfo(sa, salen,
+ addrstr, sizeof(addrstr),
+ portstr, sizeof(portstr),
+ NI_NUMERICHOST | NI_NUMERICSERV);
+ if (ret == 0) {
+ snprintf(buf, sizeof(buf),
+ (sa->sa_family == AF_INET6) ? "[%s]:%s" : "%s:%s",
+ addrstr, portstr);
+ } else {
+ sprintf(buf, "???");
+ }
+
+ strlcpy(dst, buf, size);
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runDatagramCheck(
+ JNIEnv*, jclass, jlong nethandle) {
+ const struct addrinfo kHints = {
+ .ai_flags = AI_ADDRCONFIG,
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_DGRAM,
+ .ai_protocol = IPPROTO_UDP,
+ };
+ struct addrinfo *res = NULL;
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ static const char kPort[] = "443";
+ int rval = android_getaddrinfofornetwork(handle, kHostname, kPort, &kHints, &res);
+ if (rval != 0) {
+ LOGD("android_getaddrinfofornetwork(%llu, %s) returned rval=%d errno=%d",
+ handle, kHostname, rval, errno);
+ freeaddrinfo(res);
+ return -errno;
+ }
+
+ // Rely upon getaddrinfo sorting the best destination to the front.
+ int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ if (fd < 0) {
+ LOGD("socket(%d, %d, %d) failed, errno=%d",
+ res->ai_family, res->ai_socktype, res->ai_protocol, errno);
+ freeaddrinfo(res);
+ return -errno;
+ }
+
+ rval = android_setsocknetwork(handle, fd);
+ LOGD("android_setprocnetwork(%llu, %d) returned rval=%d errno=%d",
+ handle, fd, rval, errno);
+ if (rval != 0) {
+ close(fd);
+ freeaddrinfo(res);
+ return -errno;
+ }
+
+ char addrstr[kSockaddrStrLen+1];
+ sockaddr_ntop(res->ai_addr, res->ai_addrlen, addrstr, sizeof(addrstr));
+ LOGD("Attempting connect() to %s ...", addrstr);
+
+ rval = connect(fd, res->ai_addr, res->ai_addrlen);
+ if (rval != 0) {
+ close(fd);
+ freeaddrinfo(res);
+ return -errno;
+ }
+ freeaddrinfo(res);
+
+ struct sockaddr_storage src_addr;
+ socklen_t src_addrlen = sizeof(src_addr);
+ if (getsockname(fd, (struct sockaddr *)&src_addr, &src_addrlen) != 0) {
+ close(fd);
+ return -errno;
+ }
+ sockaddr_ntop((const struct sockaddr *)&src_addr, sizeof(src_addr), addrstr, sizeof(addrstr));
+ LOGD("... from %s", addrstr);
+
+ // Don't let reads or writes block indefinitely.
+ const struct timeval timeo = { 2, 0 }; // 2 seconds
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo));
+
+ // For reference see:
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-invariants
+ uint8_t quic_packet[1200] = {
+ 0xc0, // long header
+ 0xaa, 0xda, 0xca, 0xca, // reserved-space version number
+ 0x08, // destination connection ID length
+ 0, 0, 0, 0, 0, 0, 0, 0, // 64bit connection ID
+ 0x00, // source connection ID length
+ };
+
+ arc4random_buf(quic_packet + 6, 8); // random connection ID
+
+ uint8_t response[1500];
+ ssize_t sent, rcvd;
+ static const int MAX_RETRIES = 5;
+ int i, errnum = 0;
+
+ for (i = 0; i < MAX_RETRIES; i++) {
+ sent = send(fd, quic_packet, sizeof(quic_packet), 0);
+ if (sent < (ssize_t)sizeof(quic_packet)) {
+ errnum = errno;
+ LOGD("send(QUIC packet) returned sent=%zd, errno=%d", sent, errnum);
+ close(fd);
+ return -errnum;
+ }
+
+ rcvd = recv(fd, response, sizeof(response), 0);
+ if (rcvd > 0) {
+ break;
+ } else {
+ errnum = errno;
+ LOGD("[%d/%d] recv(QUIC response) returned rcvd=%zd, errno=%d",
+ i + 1, MAX_RETRIES, rcvd, errnum);
+ }
+ }
+ if (rcvd < 15) {
+ LOGD("QUIC UDP %s: sent=%zd but rcvd=%zd, errno=%d", kPort, sent, rcvd, errnum);
+ if (rcvd <= 0) {
+ LOGD("Does this network block UDP port %s?", kPort);
+ }
+ close(fd);
+ return -EPROTO;
+ }
+
+ int conn_id_cmp = memcmp(quic_packet + 6, response + 7, 8);
+ if (conn_id_cmp != 0) {
+ LOGD("sent and received connection IDs do not match");
+ close(fd);
+ return -EPROTO;
+ }
+
+ // TODO: Replace this quick 'n' dirty test with proper QUIC-capable code.
+
+ close(fd);
+ return 0;
+}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
new file mode 100644
index 0000000..5e9af8e
--- /dev/null
+++ b/tests/cts/net/native/dns/Android.bp
@@ -0,0 +1,46 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+ name: "dns_async_defaults",
+
+ cflags: [
+ "-fstack-protector-all",
+ "-g",
+ "-Wall",
+ "-Wextra",
+ "-Werror",
+ "-Wnullable-to-nonnull-conversion",
+ "-Wsign-compare",
+ "-Wthread-safety",
+ "-Wunused-parameter",
+ ],
+ srcs: [
+ "NativeDnsAsyncTest.cpp",
+ ],
+ shared_libs: [
+ "libandroid",
+ "liblog",
+ "libutils",
+ ],
+}
+
+cc_test {
+ name: "CtsNativeNetDnsTestCases",
+ defaults: ["dns_async_defaults"],
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts-dnsresolver",
+ "mts-networking",
+ ],
+}
diff --git a/tests/cts/net/native/dns/AndroidTest.xml b/tests/cts/net/native/dns/AndroidTest.xml
new file mode 100644
index 0000000..6d03c23
--- /dev/null
+++ b/tests/cts/net/native/dns/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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 Native Network dns test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="cleanup" value="true" />
+ <option name="push" value="CtsNativeNetDnsTestCases->/data/local/tmp/CtsNativeNetDnsTestCases" />
+ <option name="append-bitness" value="true" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="CtsNativeNetDnsTestCases" />
+ <option name="runtime-hint" value="1m" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp b/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp
new file mode 100644
index 0000000..e501475
--- /dev/null
+++ b/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <error.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <poll.h> /* poll */
+#include <resolv.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include <android/multinetwork.h>
+#include <gtest/gtest.h>
+
+namespace {
+constexpr int MAXPACKET = 8 * 1024;
+constexpr int PTON_MAX = 16;
+constexpr int TIMEOUT_MS = 10000;
+
+int getAsyncResponse(int fd, int timeoutMs, int* rcode, uint8_t* buf, size_t bufLen) {
+ struct pollfd wait_fd[1];
+ wait_fd[0].fd = fd;
+ wait_fd[0].events = POLLIN;
+ short revents;
+ int ret;
+ ret = poll(wait_fd, 1, timeoutMs);
+ revents = wait_fd[0].revents;
+ if (revents & POLLIN) {
+ int n = android_res_nresult(fd, rcode, buf, bufLen);
+ // Verify that android_res_nresult() closed the fd
+ char dummy;
+ EXPECT_EQ(-1, read(fd, &dummy, sizeof dummy));
+ EXPECT_EQ(EBADF, errno);
+ return n;
+ }
+
+ return -1;
+}
+
+std::vector<std::string> extractIpAddressAnswers(uint8_t* buf, size_t bufLen, int ipType) {
+ ns_msg handle;
+ if (ns_initparse((const uint8_t*) buf, bufLen, &handle) < 0) {
+ return {};
+ }
+ const int ancount = ns_msg_count(handle, ns_s_an);
+ ns_rr rr;
+ std::vector<std::string> answers;
+ for (int i = 0; i < ancount; i++) {
+ if (ns_parserr(&handle, ns_s_an, i, &rr) < 0) {
+ continue;
+ }
+ const uint8_t* rdata = ns_rr_rdata(rr);
+ char buffer[INET6_ADDRSTRLEN];
+ if (inet_ntop(ipType, (const char*) rdata, buffer, sizeof(buffer))) {
+ answers.push_back(buffer);
+ }
+ }
+ return answers;
+}
+
+void expectAnswersValid(int fd, int ipType, int expectedRcode) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ EXPECT_GE(res, 0);
+ EXPECT_EQ(rcode, expectedRcode);
+
+ if (expectedRcode == ns_r_noerror) {
+ auto answers = extractIpAddressAnswers(buf, res, ipType);
+ EXPECT_GE(answers.size(), 0U);
+ for (auto &answer : answers) {
+ char pton[PTON_MAX];
+ EXPECT_EQ(1, inet_pton(ipType, answer.c_str(), pton));
+ }
+ }
+}
+
+void expectAnswersNotValid(int fd, int expectedErrno) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ EXPECT_EQ(expectedErrno, res);
+}
+
+} // namespace
+
+TEST (NativeDnsAsyncTest, Async_Query) {
+ // V4
+ int fd1 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(fd1, 0);
+ int fd2 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.youtube.com", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(fd2, 0);
+ expectAnswersValid(fd2, AF_INET, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET, ns_r_noerror);
+
+ // V6
+ fd1 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(fd1, 0);
+ fd2 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.youtube.com", ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(fd2, 0);
+ expectAnswersValid(fd2, AF_INET6, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET6, ns_r_noerror);
+}
+
+TEST (NativeDnsAsyncTest, Async_Send) {
+ // V4
+ uint8_t buf1[MAXPACKET] = {};
+ int len1 = res_mkquery(ns_o_query, "www.googleapis.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf1, sizeof(buf1));
+ EXPECT_GT(len1, 0);
+
+ uint8_t buf2[MAXPACKET] = {};
+ int len2 = res_mkquery(ns_o_query, "play.googleapis.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf2, sizeof(buf2));
+ EXPECT_GT(len2, 0);
+
+ int fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf1, len1, 0);
+ EXPECT_GE(fd1, 0);
+ int fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf2, len2, 0);
+ EXPECT_GE(fd2, 0);
+
+ expectAnswersValid(fd2, AF_INET, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET, ns_r_noerror);
+
+ // V6
+ memset(buf1, 0, sizeof(buf1));
+ memset(buf2, 0, sizeof(buf2));
+ len1 = res_mkquery(ns_o_query, "www.googleapis.com",
+ ns_c_in, ns_t_aaaa, nullptr, 0, nullptr, buf1, sizeof(buf1));
+ EXPECT_GT(len1, 0);
+ len2 = res_mkquery(ns_o_query, "play.googleapis.com",
+ ns_c_in, ns_t_aaaa, nullptr, 0, nullptr, buf2, sizeof(buf2));
+ EXPECT_GT(len2, 0);
+
+ fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf1, len1, 0);
+ EXPECT_GE(fd1, 0);
+ fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf2, len2, 0);
+ EXPECT_GE(fd2, 0);
+
+ expectAnswersValid(fd2, AF_INET6, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET6, ns_r_noerror);
+}
+
+TEST (NativeDnsAsyncTest, Async_NXDOMAIN) {
+ uint8_t buf[MAXPACKET] = {};
+ int len = res_mkquery(ns_o_query, "test1-nx.metric.gstatic.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf, sizeof(buf));
+ EXPECT_GT(len, 0);
+ int fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf, len, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd1, 0);
+
+ len = res_mkquery(ns_o_query, "test2-nx.metric.gstatic.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf, sizeof(buf));
+ EXPECT_GT(len, 0);
+ int fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf, len, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd2, 0);
+
+ expectAnswersValid(fd2, AF_INET, ns_r_nxdomain);
+ expectAnswersValid(fd1, AF_INET, ns_r_nxdomain);
+
+ fd1 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "test3-nx.metric.gstatic.com",
+ ns_c_in, ns_t_aaaa, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd1, 0);
+ fd2 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "test4-nx.metric.gstatic.com",
+ ns_c_in, ns_t_aaaa, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd2, 0);
+ expectAnswersValid(fd2, AF_INET6, ns_r_nxdomain);
+ expectAnswersValid(fd1, AF_INET6, ns_r_nxdomain);
+}
+
+TEST (NativeDnsAsyncTest, Async_Cancel) {
+ int fd = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_a, 0);
+ errno = 0;
+ android_res_cancel(fd);
+ int err = errno;
+ EXPECT_EQ(err, 0);
+ // DO NOT call cancel or result with the same fd more than once,
+ // otherwise it will hit fdsan double-close fd.
+}
+
+TEST (NativeDnsAsyncTest, Async_Query_MALFORMED) {
+ // Empty string to create BLOB and query, we will get empty result and rcode = 0
+ // on DNSTLS.
+ int fd = android_res_nquery(
+ NETWORK_UNSPECIFIED, "", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersValid(fd, AF_INET, ns_r_noerror);
+
+ std::string exceedingLabelQuery = "www." + std::string(70, 'g') + ".com";
+ std::string exceedingDomainQuery = "www." + std::string(255, 'g') + ".com";
+
+ fd = android_res_nquery(NETWORK_UNSPECIFIED,
+ exceedingLabelQuery.c_str(), ns_c_in, ns_t_a, 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+ fd = android_res_nquery(NETWORK_UNSPECIFIED,
+ exceedingDomainQuery.c_str(), ns_c_in, ns_t_a, 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+}
+
+TEST (NativeDnsAsyncTest, Async_Send_MALFORMED) {
+ uint8_t buf[10] = {};
+ // empty BLOB
+ int fd = android_res_nsend(NETWORK_UNSPECIFIED, buf, 10, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersNotValid(fd, -EINVAL);
+
+ std::vector<uint8_t> largeBuf(2 * MAXPACKET, 0);
+ // A buffer larger than 8KB
+ fd = android_res_nsend(
+ NETWORK_UNSPECIFIED, largeBuf.data(), largeBuf.size(), 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+
+ // 5000 bytes filled with 0. This returns EMSGSIZE because FrameworkListener limits the size of
+ // commands to 4096 bytes.
+ fd = android_res_nsend(NETWORK_UNSPECIFIED, largeBuf.data(), 5000, 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+
+ // 500 bytes filled with 0
+ fd = android_res_nsend(NETWORK_UNSPECIFIED, largeBuf.data(), 500, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersNotValid(fd, -EINVAL);
+
+ // 5000 bytes filled with 0xFF
+ std::vector<uint8_t> ffBuf(5000, 0xFF);
+ fd = android_res_nsend(
+ NETWORK_UNSPECIFIED, ffBuf.data(), ffBuf.size(), 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+
+ // 500 bytes filled with 0xFF
+ fd = android_res_nsend(NETWORK_UNSPECIFIED, ffBuf.data(), 500, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersNotValid(fd, -EINVAL);
+}
diff --git a/tests/cts/net/native/qtaguid/Android.bp b/tests/cts/net/native/qtaguid/Android.bp
new file mode 100644
index 0000000..68bb14d
--- /dev/null
+++ b/tests/cts/net/native/qtaguid/Android.bp
@@ -0,0 +1,57 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Build the unit tests.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+ name: "CtsNativeNetTestCases",
+
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+
+ srcs: ["src/NativeQtaguidTest.cpp"],
+
+ shared_libs: [
+ "libutils",
+ "liblog",
+ ],
+
+ static_libs: [
+ "libgtest",
+ "libqtaguid",
+ ],
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+ cflags: [
+ "-Werror",
+ "-Wall",
+ ],
+
+}
diff --git a/tests/cts/net/native/qtaguid/AndroidTest.xml b/tests/cts/net/native/qtaguid/AndroidTest.xml
new file mode 100644
index 0000000..fa4b2cf
--- /dev/null
+++ b/tests/cts/net/native/qtaguid/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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 Native Network xt_qtaguid test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="cleanup" value="true" />
+ <option name="push" value="CtsNativeNetTestCases->/data/local/tmp/CtsNativeNetTestCases" />
+ <option name="append-bitness" value="true" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="CtsNativeNetTestCases" />
+ <option name="runtime-hint" value="1m" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp b/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp
new file mode 100644
index 0000000..7dc6240
--- /dev/null
+++ b/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <arpa/inet.h>
+#include <error.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include <gtest/gtest.h>
+#include <qtaguid/qtaguid.h>
+
+int canAccessQtaguidFile() {
+ int fd = open("/proc/net/xt_qtaguid/ctrl", O_RDONLY | O_CLOEXEC);
+ close(fd);
+ return fd != -1;
+}
+
+#define SKIP_IF_QTAGUID_NOT_SUPPORTED() \
+ do { \
+ int res = canAccessQtaguidFile(); \
+ ASSERT_LE(0, res); \
+ if (!res) { \
+ GTEST_LOG_(INFO) << "This test is skipped since kernel may not have the module\n"; \
+ return; \
+ } \
+ } while (0)
+
+int getCtrlSkInfo(int tag, uid_t uid, uint64_t* sk_addr, int* ref_cnt) {
+ FILE *fp;
+ fp = fopen("/proc/net/xt_qtaguid/ctrl", "r");
+ if (!fp)
+ return -ENOENT;
+ uint64_t full_tag = (uint64_t)tag << 32 | uid;
+ char pattern[40];
+ snprintf(pattern, sizeof(pattern), " tag=0x%" PRIx64 " (uid=%" PRIu32 ")", full_tag, uid);
+
+ size_t len;
+ char *line_buffer = NULL;
+ while(getline(&line_buffer, &len, fp) != -1) {
+ if (strstr(line_buffer, pattern) == NULL)
+ continue;
+ int res;
+ pid_t dummy_pid;
+ uint64_t k_tag;
+ uint32_t k_uid;
+ const int TOTAL_PARAM = 5;
+ res = sscanf(line_buffer, "sock=%" PRIx64 " tag=0x%" PRIx64 " (uid=%" PRIu32 ") "
+ "pid=%u f_count=%u", sk_addr, &k_tag, &k_uid,
+ &dummy_pid, ref_cnt);
+ if (!(res == TOTAL_PARAM && k_tag == full_tag && k_uid == uid))
+ return -EINVAL;
+ free(line_buffer);
+ return 0;
+ }
+ free(line_buffer);
+ return -ENOENT;
+}
+
+void checkNoSocketPointerLeaks(int family) {
+ int sockfd = socket(family, SOCK_STREAM, 0);
+ uid_t uid = getuid();
+ int tag = arc4random();
+ int ref_cnt;
+ uint64_t sk_addr;
+ uint64_t expect_addr = 0;
+
+ EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid));
+ EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &sk_addr, &ref_cnt));
+ EXPECT_EQ(expect_addr, sk_addr);
+ close(sockfd);
+ EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &sk_addr, &ref_cnt));
+}
+
+TEST (NativeQtaguidTest, close_socket_without_untag) {
+ SKIP_IF_QTAGUID_NOT_SUPPORTED();
+
+ int sockfd = socket(AF_INET, SOCK_STREAM, 0);
+ uid_t uid = getuid();
+ int tag = arc4random();
+ int ref_cnt;
+ uint64_t dummy_sk;
+ EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid));
+ EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+ EXPECT_EQ(2, ref_cnt);
+ close(sockfd);
+ EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+}
+
+TEST (NativeQtaguidTest, close_socket_without_untag_ipv6) {
+ SKIP_IF_QTAGUID_NOT_SUPPORTED();
+
+ int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
+ uid_t uid = getuid();
+ int tag = arc4random();
+ int ref_cnt;
+ uint64_t dummy_sk;
+ EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid));
+ EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+ EXPECT_EQ(2, ref_cnt);
+ close(sockfd);
+ EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+}
+
+TEST (NativeQtaguidTest, no_socket_addr_leak) {
+ SKIP_IF_QTAGUID_NOT_SUPPORTED();
+
+ checkNoSocketPointerLeaks(AF_INET);
+ checkNoSocketPointerLeaks(AF_INET6);
+}
+
+int main(int argc, char **argv) {
+ testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/tests/cts/net/src/android/net/cts/AirplaneModeTest.java b/tests/cts/net/src/android/net/cts/AirplaneModeTest.java
new file mode 100644
index 0000000..524e549
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/AirplaneModeTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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 android.net.cts;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.lang.Thread;
+
+@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+public class AirplaneModeTest extends AndroidTestCase {
+ private static final String TAG = "AirplaneModeTest";
+ private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
+ private static final String FEATURE_WIFI = "android.hardware.wifi";
+ private static final int TIMEOUT_MS = 10 * 1000;
+ private boolean mHasFeature;
+ private Context mContext;
+ private ContentResolver resolver;
+
+ public void setup() {
+ mContext= getContext();
+ resolver = mContext.getContentResolver();
+ mHasFeature = (mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH)
+ || mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI));
+ }
+
+ public void testAirplaneMode() {
+ setup();
+ if (!mHasFeature) {
+ Log.i(TAG, "The device doesn't support network bluetooth or wifi feature");
+ return;
+ }
+
+ for (int testCount = 0; testCount < 2; testCount++) {
+ if (!doOneTest()) {
+ fail("Airplane mode failed to change in " + TIMEOUT_MS + "msec");
+ return;
+ }
+ }
+ }
+
+ private boolean doOneTest() {
+ boolean airplaneModeOn = isAirplaneModeOn();
+ setAirplaneModeOn(!airplaneModeOn);
+
+ try {
+ Thread.sleep(TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Sleep time interrupted.", e);
+ }
+
+ if (airplaneModeOn == isAirplaneModeOn()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void setAirplaneModeOn(boolean enabling) {
+ // Change the system setting for airplane mode
+ Settings.Global.putInt(resolver, Settings.Global.AIRPLANE_MODE_ON, enabling ? 1 : 0);
+ }
+
+ private boolean isAirplaneModeOn() {
+ // Read the system setting for airplane mode
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
new file mode 100644
index 0000000..a889c41
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.CONNECTIVITY_INTERNAL
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.Uri
+import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
+import android.net.cts.util.CtsNetUtils
+import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
+import android.net.wifi.WifiManager
+import android.os.Build
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.text.TextUtils
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.RecorderCallback
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestHttpServer.Request
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.isDevSdkInRange
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD.Response.Status
+import junit.framework.AssertionFailedError
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.runner.RunWith
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.test.Test
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+private const val TEST_HTTPS_URL_PATH = "/https_path"
+private const val TEST_HTTP_URL_PATH = "/http_path"
+private const val TEST_PORTAL_URL_PATH = "/portal_path"
+
+private const val LOCALHOST_HOSTNAME = "localhost"
+
+// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
+private const val WIFI_CONNECT_TIMEOUT_MS = 120_000L
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
+ try {
+ return get(timeoutMs, TimeUnit.MILLISECONDS)
+ } catch (e: TimeoutException) {
+ throw AssertionFailedError(message)
+ }
+}
+
+@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+@RunWith(AndroidJUnit4::class)
+class CaptivePortalTest {
+ private val context: android.content.Context by lazy { getInstrumentation().context }
+ private val wm by lazy { context.getSystemService(WifiManager::class.java) }
+ private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+ private val pm by lazy { context.packageManager }
+ private val utils by lazy { CtsNetUtils(context) }
+
+ private val server = TestHttpServer("localhost")
+
+ @Before
+ fun setUp() {
+ runAsShell(READ_DEVICE_CONFIG) {
+ // Verify that the test URLs are not normally set on the device, but do not fail if the
+ // test URLs are set to what this test uses (URLs on localhost), in case the test was
+ // interrupted manually and rerun.
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+ }
+ clearValidationTestUrlsDeviceConfig()
+ server.start()
+ }
+
+ @After
+ fun tearDown() {
+ clearValidationTestUrlsDeviceConfig()
+ if (pm.hasSystemFeature(FEATURE_WIFI)) {
+ reconnectWifi()
+ }
+ server.stop()
+ }
+
+ private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+ val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+ assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+ "$urlKey must not be set in production scenarios (current value: $url)")
+ }
+
+ @Test
+ fun testCaptivePortalIsNotDefaultNetwork() {
+ assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
+ assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
+ utils.ensureWifiConnected()
+ val cellNetwork = utils.connectToCell()
+
+ // Verify cell network is validated
+ val cellReq = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS)
+ cm.registerNetworkCallback(cellReq, cellCb)
+ val cb = cellCb.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.CapabilitiesChanged> {
+ it.network == cellNetwork && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ assertNotNull(cb, "Mobile network $cellNetwork has no access to the internet. " +
+ "Check the mobile data connection.")
+
+ // Have network validation use a local server that serves a HTTPS error / HTTP redirect
+ server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
+ content = "Test captive portal content")
+ server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
+ server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT,
+ locationHeader = makeUrl(TEST_PORTAL_URL_PATH))
+ setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
+ setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+ // URL expiration needs to be in the next 10 minutes
+ assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
+ setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
+
+ // Wait for a captive portal to be detected on the network
+ val wifiNetworkFuture = CompletableFuture<Network>()
+ val wifiCb = object : NetworkCallback() {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ nc: NetworkCapabilities
+ ) {
+ if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
+ wifiNetworkFuture.complete(network)
+ }
+ }
+ }
+ cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
+
+ try {
+ reconnectWifi()
+ val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
+ "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
+
+ val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
+ "portal was detected and another network (mobile data) can provide internet " +
+ "access."
+ assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+
+ val startPortalAppPermission =
+ if (isDevSdkInRange(0, Build.VERSION_CODES.Q)) CONNECTIVITY_INTERNAL
+ else NETWORK_SETTINGS
+ runAsShell(startPortalAppPermission) { 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.
+ assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) {
+ it.path == TEST_PORTAL_URL_PATH
+ }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " +
+ "after startCaptivePortalApp.")
+
+ assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+ } finally {
+ cm.unregisterNetworkCallback(wifiCb)
+ server.stop()
+ // disconnectFromCell should be called after connectToCell
+ utils.disconnectFromCell()
+ }
+ }
+
+ /**
+ * Create a URL string that, when fetched, will hit the test server with the given URL [path].
+ */
+ private fun makeUrl(path: String) = "http://localhost:${server.listeningPort}" + path
+
+ private fun reconnectWifi() {
+ utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
+ utils.ensureWifiConnected()
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
new file mode 100644
index 0000000..ccbdbd3
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -0,0 +1,636 @@
+/*
+ * 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 android.net.cts;
+
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityDiagnosticsManager.persistableBundleEquals;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.util.ArrayUtils;
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.SkipPresubmit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.Q) // ConnectivityDiagnosticsManager did not exist in Q
+@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
+public class ConnectivityDiagnosticsManagerTest {
+ private static final int CALLBACK_TIMEOUT_MILLIS = 5000;
+ private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500;
+ private static final long TIMESTAMP = 123456789L;
+ private static final int DNS_CONSECUTIVE_TIMEOUTS = 5;
+ private static final int COLLECTION_PERIOD_MILLIS = 5000;
+ private static final int FAIL_RATE_PERCENTAGE = 100;
+ private static final int UNKNOWN_DETECTION_METHOD = 4;
+ private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
+ private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
+ private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+
+ private static final Executor INLINE_EXECUTOR = x -> x.run();
+
+ private static final NetworkRequest TEST_NETWORK_REQUEST =
+ new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .removeCapability(NET_CAPABILITY_NOT_VPN)
+ .build();
+
+ private static final String SHA_256 = "SHA-256";
+
+ private static final NetworkRequest CELLULAR_NETWORK_REQUEST =
+ new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+
+ private static final IBinder BINDER = new Binder();
+
+ // Lock for accessing Shell Permissions. Use of this lock around adoptShellPermissionIdentity,
+ // runWithShellPermissionIdentity, and callWithShellPermissionIdentity ensures Shell Permission
+ // is not interrupted by another operation (which would drop all previously adopted
+ // permissions).
+ private Object mShellPermissionsIdentityLock = new Object();
+
+ private Context mContext;
+ private ConnectivityManager mConnectivityManager;
+ private ConnectivityDiagnosticsManager mCdm;
+ private CarrierConfigManager mCarrierConfigManager;
+ private PackageManager mPackageManager;
+ private TelephonyManager mTelephonyManager;
+
+ // Callback used to keep TestNetworks up when there are no other outstanding NetworkRequests
+ // for it.
+ private TestNetworkCallback mTestNetworkCallback;
+ private Network mTestNetwork;
+ private ParcelFileDescriptor mTestNetworkFD;
+
+ private List<TestConnectivityDiagnosticsCallback> mRegisteredCallbacks;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getContext();
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ mCdm = mContext.getSystemService(ConnectivityDiagnosticsManager.class);
+ mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+ mPackageManager = mContext.getPackageManager();
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+
+ mTestNetworkCallback = new TestNetworkCallback();
+ mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, mTestNetworkCallback);
+
+ mRegisteredCallbacks = new ArrayList<>();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mConnectivityManager.unregisterNetworkCallback(mTestNetworkCallback);
+ if (mTestNetwork != null) {
+ runWithShellPermissionIdentity(() -> {
+ final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
+ tnm.teardownTestNetwork(mTestNetwork);
+ });
+ mTestNetwork = null;
+ }
+
+ if (mTestNetworkFD != null) {
+ mTestNetworkFD.close();
+ mTestNetworkFD = null;
+ }
+
+ for (TestConnectivityDiagnosticsCallback cb : mRegisteredCallbacks) {
+ mCdm.unregisterConnectivityDiagnosticsCallback(cb);
+ }
+ }
+
+ @Test
+ public void testRegisterConnectivityDiagnosticsCallback() throws Exception {
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ cb.assertNoCallback();
+ }
+
+ @SkipPresubmit(reason = "Flaky: b/159718782; add to presubmit after fixing")
+ @Test
+ public void testRegisterCallbackWithCarrierPrivileges() throws Exception {
+ assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
+
+ final int subId = SubscriptionManager.getDefaultSubscriptionId();
+ if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ fail("Need an active subscription. Please ensure that the device has working mobile"
+ + " data.");
+ }
+
+ final CarrierConfigReceiver carrierConfigReceiver = new CarrierConfigReceiver(subId);
+ mContext.registerReceiver(
+ carrierConfigReceiver,
+ new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+
+ final TestNetworkCallback testNetworkCallback = new TestNetworkCallback();
+
+ try {
+ doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
+ subId, carrierConfigReceiver, testNetworkCallback);
+ } finally {
+ runWithShellPermissionIdentity(
+ () -> mCarrierConfigManager.overrideConfig(subId, null),
+ android.Manifest.permission.MODIFY_PHONE_STATE);
+ mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+ mContext.unregisterReceiver(carrierConfigReceiver);
+ }
+ }
+
+ private String getCertHashForThisPackage() throws Exception {
+ final PackageInfo pkgInfo =
+ mPackageManager.getPackageInfo(
+ mContext.getOpPackageName(), PackageManager.GET_SIGNATURES);
+ final MessageDigest md = MessageDigest.getInstance(SHA_256);
+ final byte[] certHash = md.digest(pkgInfo.signatures[0].toByteArray());
+ return IccUtils.bytesToHexString(certHash);
+ }
+
+ private void doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
+ int subId,
+ @NonNull CarrierConfigReceiver carrierConfigReceiver,
+ @NonNull TestNetworkCallback testNetworkCallback)
+ throws Exception {
+ final PersistableBundle carrierConfigs = new PersistableBundle();
+ carrierConfigs.putStringArray(
+ CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+ new String[] {getCertHashForThisPackage()});
+
+ synchronized (mShellPermissionsIdentityLock) {
+ runWithShellPermissionIdentity(
+ () -> {
+ mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
+ mCarrierConfigManager.notifyConfigChangedForSubId(subId);
+ },
+ android.Manifest.permission.MODIFY_PHONE_STATE);
+ }
+
+ // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
+ // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
+ // permissions are updated.
+ synchronized (mShellPermissionsIdentityLock) {
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.requestNetwork(
+ CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+ android.Manifest.permission.CONNECTIVITY_INTERNAL);
+ }
+
+ final Network network = testNetworkCallback.waitForAvailable();
+ assertNotNull(network);
+
+ assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
+ carrierConfigReceiver.waitForCarrierConfigChanged());
+ assertTrue("Don't have Carrier Privileges after adding cert for this package",
+ mTelephonyManager.createForSubscriptionId(subId).hasCarrierPrivileges());
+
+ // Wait for CarrierPrivilegesTracker to receive the ACTION_CARRIER_CONFIG_CHANGED
+ // broadcast. CPT then needs to update the corresponding DataConnection, which then
+ // updates ConnectivityService. Unfortunately, this update to the NetworkCapabilities in
+ // CS does not trigger NetworkCallback#onCapabilitiesChanged as changing the
+ // administratorUids is not a publicly visible change. In lieu of a better signal to
+ // detministically wait for, use Thread#sleep here.
+ // TODO(b/157949581): replace this Thread#sleep with a deterministic signal
+ Thread.sleep(DELAY_FOR_ADMIN_UIDS_MILLIS);
+
+ final TestConnectivityDiagnosticsCallback connDiagsCallback =
+ createAndRegisterConnectivityDiagnosticsCallback(CELLULAR_NETWORK_REQUEST);
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(network).getInterfaceName();
+ connDiagsCallback.expectOnConnectivityReportAvailable(
+ network, interfaceName, TRANSPORT_CELLULAR);
+ connDiagsCallback.assertNoCallback();
+ }
+
+ @Test
+ public void testRegisterDuplicateConnectivityDiagnosticsCallback() {
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ try {
+ mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb);
+ fail("Registering the same callback twice should throw an IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void testUnregisterConnectivityDiagnosticsCallback() {
+ final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback();
+ mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb);
+ mCdm.unregisterConnectivityDiagnosticsCallback(cb);
+ }
+
+ @Test
+ public void testUnregisterUnknownConnectivityDiagnosticsCallback() {
+ // Expected to silently ignore the unregister() call
+ mCdm.unregisterConnectivityDiagnosticsCallback(new TestConnectivityDiagnosticsCallback());
+ }
+
+ @Test
+ public void testOnConnectivityReportAvailable() throws Exception {
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ cb.assertNoCallback();
+ }
+
+ @Test
+ public void testOnDataStallSuspected_DnsEvents() throws Exception {
+ final PersistableBundle extras = new PersistableBundle();
+ extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, DNS_CONSECUTIVE_TIMEOUTS);
+
+ verifyOnDataStallSuspected(DETECTION_METHOD_DNS_EVENTS, TIMESTAMP, extras);
+ }
+
+ @Test
+ public void testOnDataStallSuspected_TcpMetrics() throws Exception {
+ final PersistableBundle extras = new PersistableBundle();
+ extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS, COLLECTION_PERIOD_MILLIS);
+ extras.putInt(KEY_TCP_PACKET_FAIL_RATE, FAIL_RATE_PERCENTAGE);
+
+ verifyOnDataStallSuspected(DETECTION_METHOD_TCP_METRICS, TIMESTAMP, extras);
+ }
+
+ @Test
+ public void testOnDataStallSuspected_UnknownDetectionMethod() throws Exception {
+ verifyOnDataStallSuspected(
+ UNKNOWN_DETECTION_METHOD,
+ FILTERED_UNKNOWN_DETECTION_METHOD,
+ TIMESTAMP,
+ PersistableBundle.EMPTY);
+ }
+
+ private void verifyOnDataStallSuspected(
+ int detectionMethod, long timestampMillis, @NonNull PersistableBundle extras)
+ throws Exception {
+ // Input detection method is expected to match received detection method
+ verifyOnDataStallSuspected(detectionMethod, detectionMethod, timestampMillis, extras);
+ }
+
+ private void verifyOnDataStallSuspected(
+ int inputDetectionMethod,
+ int expectedDetectionMethod,
+ long timestampMillis,
+ @NonNull PersistableBundle extras)
+ throws Exception {
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.simulateDataStall(
+ inputDetectionMethod, timestampMillis, mTestNetwork, extras),
+ android.Manifest.permission.MANAGE_TEST_NETWORKS);
+
+ cb.expectOnDataStallSuspected(
+ mTestNetwork, interfaceName, expectedDetectionMethod, timestampMillis, extras);
+ cb.assertNoCallback();
+ }
+
+ @Test
+ public void testOnNetworkConnectivityReportedTrue() throws Exception {
+ verifyOnNetworkConnectivityReported(true /* hasConnectivity */);
+ }
+
+ @Test
+ public void testOnNetworkConnectivityReportedFalse() throws Exception {
+ verifyOnNetworkConnectivityReported(false /* hasConnectivity */);
+ }
+
+ private void verifyOnNetworkConnectivityReported(boolean hasConnectivity) throws Exception {
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ // onConnectivityReportAvailable always invoked when the test network is established
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ cb.assertNoCallback();
+
+ mConnectivityManager.reportNetworkConnectivity(mTestNetwork, hasConnectivity);
+
+ cb.expectOnNetworkConnectivityReported(mTestNetwork, hasConnectivity);
+
+ // if hasConnectivity does not match the network's known connectivity, it will be
+ // revalidated which will trigger another onConnectivityReportAvailable callback.
+ if (!hasConnectivity) {
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ }
+
+ cb.assertNoCallback();
+ }
+
+ private TestConnectivityDiagnosticsCallback createAndRegisterConnectivityDiagnosticsCallback(
+ NetworkRequest request) {
+ final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback();
+ mCdm.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, cb);
+ mRegisteredCallbacks.add(cb);
+ return cb;
+ }
+
+ /**
+ * Registers a test NetworkAgent with ConnectivityService with limited capabilities, which leads
+ * to the Network being validated.
+ */
+ @NonNull
+ private TestNetworkInterface setUpTestNetwork() throws Exception {
+ final int[] administratorUids = new int[] {Process.myUid()};
+ return callWithShellPermissionIdentity(
+ () -> {
+ final TestNetworkManager tnm =
+ mContext.getSystemService(TestNetworkManager.class);
+ final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]);
+ tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER);
+ return tni;
+ });
+ }
+
+ private static class TestConnectivityDiagnosticsCallback
+ extends ConnectivityDiagnosticsCallback {
+ private final ArrayTrackRecord<Object>.ReadHead mHistory =
+ new ArrayTrackRecord<Object>().newReadHead();
+
+ @Override
+ public void onConnectivityReportAvailable(ConnectivityReport report) {
+ mHistory.add(report);
+ }
+
+ @Override
+ public void onDataStallSuspected(DataStallReport report) {
+ mHistory.add(report);
+ }
+
+ @Override
+ public void onNetworkConnectivityReported(Network network, boolean hasConnectivity) {
+ mHistory.add(new Pair<Network, Boolean>(network, hasConnectivity));
+ }
+
+ public void expectOnConnectivityReportAvailable(
+ @NonNull Network network, @NonNull String interfaceName) {
+ expectOnConnectivityReportAvailable(network, interfaceName, TRANSPORT_TEST);
+ }
+
+ public void expectOnConnectivityReportAvailable(
+ @NonNull Network network, @NonNull String interfaceName, int transportType) {
+ final ConnectivityReport result =
+ (ConnectivityReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+ assertEquals(network, result.getNetwork());
+
+ final NetworkCapabilities nc = result.getNetworkCapabilities();
+ assertNotNull(nc);
+ assertTrue(nc.hasTransport(transportType));
+ assertNotNull(result.getLinkProperties());
+ assertEquals(interfaceName, result.getLinkProperties().getInterfaceName());
+
+ final PersistableBundle extras = result.getAdditionalInfo();
+ assertTrue(extras.containsKey(KEY_NETWORK_VALIDATION_RESULT));
+ final int validationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
+ assertEquals("Network validation result is not 'valid'",
+ NETWORK_VALIDATION_RESULT_VALID, validationResult);
+
+ assertTrue(extras.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK));
+ final int probesSucceeded = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
+ assertTrue("PROBES_SUCCEEDED mask not in expected range", probesSucceeded >= 0);
+
+ assertTrue(extras.containsKey(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK));
+ final int probesAttempted = extras.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK);
+ assertTrue("PROBES_ATTEMPTED mask not in expected range", probesAttempted >= 0);
+ }
+
+ public void expectOnDataStallSuspected(
+ @NonNull Network network,
+ @NonNull String interfaceName,
+ int detectionMethod,
+ long timestampMillis,
+ @NonNull PersistableBundle extras) {
+ final DataStallReport result =
+ (DataStallReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+ assertEquals(network, result.getNetwork());
+ assertEquals(detectionMethod, result.getDetectionMethod());
+ assertEquals(timestampMillis, result.getReportTimestamp());
+
+ final NetworkCapabilities nc = result.getNetworkCapabilities();
+ assertNotNull(nc);
+ assertTrue(nc.hasTransport(TRANSPORT_TEST));
+ assertNotNull(result.getLinkProperties());
+ assertEquals(interfaceName, result.getLinkProperties().getInterfaceName());
+
+ assertTrue(persistableBundleEquals(extras, result.getStallDetails()));
+ }
+
+ public void expectOnNetworkConnectivityReported(
+ @NonNull Network network, boolean hasConnectivity) {
+ final Pair<Network, Boolean> result =
+ (Pair<Network, Boolean>) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+ assertEquals(network, result.first /* network */);
+ assertEquals(hasConnectivity, result.second /* hasConnectivity */);
+ }
+
+ public void assertNoCallback() {
+ // If no more callbacks exist, there should be nothing left in the ReadHead
+ assertNull("Unexpected event in history",
+ mHistory.poll(NO_CALLBACK_INVOKED_TIMEOUT, x -> true));
+ }
+ }
+
+ private class CarrierConfigReceiver extends BroadcastReceiver {
+ // CountDownLatch used to wait for this BroadcastReceiver to be notified of a CarrierConfig
+ // change. This latch will be counted down if a broadcast indicates this package has carrier
+ // configs, or if an Exception occurs in #onReceive.
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final int mSubId;
+
+ // #onReceive may encounter Exceptions while running on the Process' main Thread and
+ // #waitForCarrierConfigChanged checks the cached Exception from the test Thread. These
+ // Exceptions must be cached and thrown later, as throwing on the Process' main Thread will
+ // crash the process and cause other tests to fail.
+ private Exception mOnReceiveException;
+
+ CarrierConfigReceiver(int subId) {
+ mSubId = subId;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+ // Received an incorrect broadcast - ignore
+ return;
+ }
+
+ final int subId =
+ intent.getIntExtra(
+ CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX,
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+ if (mSubId != subId) {
+ // Received a broadcast for the wrong subId - ignore
+ return;
+ }
+
+ final PersistableBundle carrierConfigs;
+ try {
+ synchronized (mShellPermissionsIdentityLock) {
+ carrierConfigs = callWithShellPermissionIdentity(
+ () -> mCarrierConfigManager.getConfigForSubId(subId),
+ android.Manifest.permission.READ_PHONE_STATE);
+ }
+ } catch (Exception exception) {
+ // callWithShellPermissionIdentity() threw an Exception - cache it and allow
+ // waitForCarrierConfigChanged() to throw it
+ mOnReceiveException = exception;
+ mLatch.countDown();
+ return;
+ }
+
+ if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfigs)) {
+ // Configs are not for an identified carrier (meaning they are defaults) - ignore
+ return;
+ }
+
+ final String[] certs = carrierConfigs.getStringArray(
+ CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY);
+ try {
+ if (ArrayUtils.contains(certs, getCertHashForThisPackage())) {
+ // Received an update for this package's cert hash - countdown and exit
+ mLatch.countDown();
+ }
+ // Broadcast is for the right subId, but does not show this package as Carrier
+ // Privileged. Keep waiting for a broadcast that indicates Carrier Privileges.
+ } catch (Exception exception) {
+ // getCertHashForThisPackage() threw an Exception - cache it and allow
+ // waitForCarrierConfigChanged() to throw it
+ mOnReceiveException = exception;
+ mLatch.countDown();
+ }
+ }
+
+ /**
+ * Waits for the CarrierConfig changed broadcast to reach this CarrierConfigReceiver.
+ *
+ * <p>Must be called from the Test Thread.
+ *
+ * @throws Exception if an Exception occurred during any #onReceive invocation
+ */
+ boolean waitForCarrierConfigChanged() throws Exception {
+ final boolean result = mLatch.await(CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+
+ if (mOnReceiveException != null) {
+ throw mOnReceiveException;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
new file mode 100644
index 0000000..90efba0
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -0,0 +1,1998 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_ETHERNET;
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_USB_HOST;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY;
+import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
+import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
+import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
+import static android.net.cts.util.CtsNetUtils.TEST_HOST;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_UNSPEC;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+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.testutils.MiscAsserts.assertThrows;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+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.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.app.Instrumentation;
+import android.app.PendingIntent;
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.InetAddresses;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkInfo.State;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkUtils;
+import android.net.ProxyInfo;
+import android.net.SocketKeepalive;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.cts.util.CtsNetUtils;
+import android.net.util.KeepaliveUtils;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.VintfRuntimeInfo;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Range;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.NetworkInformationShimImpl;
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.testutils.CompatUtil;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.SkipPresubmit;
+import com.android.testutils.TestableNetworkCallback;
+
+import junit.framework.AssertionFailedError;
+
+import libcore.io.Streams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityManagerTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
+
+ public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
+ public static final int TYPE_WIFI = ConnectivityManager.TYPE_WIFI;
+
+ private static final int HOST_ADDRESS = 0x7f000001;// represent ip 127.0.0.1
+ private static final int KEEPALIVE_CALLBACK_TIMEOUT_MS = 2000;
+ private static final int INTERVAL_KEEPALIVE_RETRY_MS = 500;
+ private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
+ private static final int MIN_KEEPALIVE_INTERVAL = 10;
+
+ private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
+ private static final int NO_CALLBACK_TIMEOUT_MS = 100;
+ private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
+ private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
+ // device could have only one interface: data, wifi.
+ private static final int MIN_NUM_NETWORK_TYPES = 1;
+
+ // Airplane Mode BroadcastReceiver Timeout
+ private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L;
+
+ // Minimum supported keepalive counts for wifi and cellular.
+ public static final int MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT = 1;
+ public static final int MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT = 3;
+
+ private static final String NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME =
+ "config_networkMeteredMultipathPreference";
+ private static final String KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME =
+ "config_allowedUnprivilegedKeepalivePerUid";
+ private static final String KEEPALIVE_RESERVED_PER_SLOT_RES_NAME =
+ "config_reservedPrivilegedKeepaliveSlots";
+
+ private static final LinkAddress TEST_LINKADDR = new LinkAddress(
+ InetAddresses.parseNumericAddress("2001:db8::8"), 64);
+
+ private Context mContext;
+ private Instrumentation mInstrumentation;
+ private ConnectivityManager mCm;
+ private ConnectivityManagerShim mCmShim;
+ private WifiManager mWifiManager;
+ private PackageManager mPackageManager;
+ private final ArraySet<Integer> mNetworkTypes = new ArraySet<>();
+ private UiAutomation mUiAutomation;
+ private CtsNetUtils mCtsNetUtils;
+
+ // Used for cleanup purposes.
+ private final List<Range<Integer>> mVpnRequiredUidRanges = new ArrayList<>();
+
+ @Before
+ public void setUp() throws Exception {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = mInstrumentation.getContext();
+ mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
+ mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ mPackageManager = mContext.getPackageManager();
+ mCtsNetUtils = new CtsNetUtils(mContext);
+
+ if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */,
+ Build.VERSION_CODES.R /* maxInclusive */)) {
+ addLegacySupportedNetworkTypes();
+ } else {
+ addSupportedNetworkTypes();
+ }
+
+ mUiAutomation = mInstrumentation.getUiAutomation();
+
+ assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork());
+ }
+
+ private void addLegacySupportedNetworkTypes() {
+ // Network type support as expected for android R-
+ // Get com.android.internal.R.array.networkAttributes
+ int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
+ String[] naStrings = mContext.getResources().getStringArray(resId);
+ boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false);
+ for (String naString : naStrings) {
+ try {
+ final String[] splitConfig = naString.split(",");
+ // Format was name,type,radio,priority,restoreTime,dependencyMet
+ final int type = Integer.parseInt(splitConfig[1]);
+ if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(type)) {
+ continue;
+ }
+ mNetworkTypes.add(type);
+ } catch (Exception e) {}
+ }
+ }
+
+ private void addSupportedNetworkTypes() {
+ final PackageManager pm = mContext.getPackageManager();
+ if (pm.hasSystemFeature(FEATURE_WIFI)) {
+ mNetworkTypes.add(TYPE_WIFI);
+ }
+ if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) {
+ mNetworkTypes.add(TYPE_WIFI_P2P);
+ }
+ if (mContext.getSystemService(TelephonyManager.class).isDataCapable()) {
+ mNetworkTypes.add(TYPE_MOBILE);
+ mNetworkTypes.add(TYPE_MOBILE_MMS);
+ mNetworkTypes.add(TYPE_MOBILE_SUPL);
+ mNetworkTypes.add(TYPE_MOBILE_DUN);
+ mNetworkTypes.add(TYPE_MOBILE_HIPRI);
+ mNetworkTypes.add(TYPE_MOBILE_FOTA);
+ mNetworkTypes.add(TYPE_MOBILE_IMS);
+ mNetworkTypes.add(TYPE_MOBILE_CBS);
+ mNetworkTypes.add(TYPE_MOBILE_IA);
+ mNetworkTypes.add(TYPE_MOBILE_EMERGENCY);
+ }
+ if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) {
+ mNetworkTypes.add(TYPE_BLUETOOTH);
+ }
+ if (pm.hasSystemFeature(FEATURE_WATCH)) {
+ mNetworkTypes.add(TYPE_PROXY);
+ }
+ if (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) {
+ mNetworkTypes.add(TYPE_ETHERNET);
+ }
+ mNetworkTypes.add(TYPE_VPN);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Release any NetworkRequests filed to connect mobile data.
+ if (mCtsNetUtils.cellConnectAttempted()) {
+ mCtsNetUtils.disconnectFromCell();
+ }
+
+ if (TestUtils.shouldTestSApis()) {
+ runWithShellPermissionIdentity(
+ () -> mCmShim.setRequireVpnForUids(false, mVpnRequiredUidRanges),
+ NETWORK_SETTINGS);
+ }
+
+ // All tests in this class require a working Internet connection as they start. Make
+ // sure there is still one as they end that's ready to use for the next test to use.
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerDefaultNetworkCallback(callback);
+ try {
+ assertNotNull("Couldn't restore Internet connectivity", callback.waitForAvailable());
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ @Test
+ public void testIsNetworkTypeValid() {
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_MMS));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_SUPL));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_DUN));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_HIPRI));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIMAX));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_BLUETOOTH));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_DUMMY));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_ETHERNET));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_FOTA));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IMS));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_CBS));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI_P2P));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IA));
+ assertFalse(mCm.isNetworkTypeValid(-1));
+ assertTrue(mCm.isNetworkTypeValid(0));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE));
+ assertFalse(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE+1));
+
+ NetworkInfo[] ni = mCm.getAllNetworkInfo();
+
+ for (NetworkInfo n: ni) {
+ assertTrue(ConnectivityManager.isNetworkTypeValid(n.getType()));
+ }
+
+ }
+
+ @Test
+ public void testSetNetworkPreference() {
+ // getNetworkPreference() and setNetworkPreference() are both deprecated so they do
+ // not preform any action. Verify they are at least still callable.
+ mCm.setNetworkPreference(mCm.getNetworkPreference());
+ }
+
+ @Test
+ public void testGetActiveNetworkInfo() {
+ NetworkInfo ni = mCm.getActiveNetworkInfo();
+
+ assertNotNull("You must have an active network connection to complete CTS", ni);
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType()));
+ assertTrue(ni.getState() == State.CONNECTED);
+ }
+
+ @Test
+ public void testGetActiveNetwork() {
+ Network network = mCm.getActiveNetwork();
+ assertNotNull("You must have an active network connection to complete CTS", network);
+
+ NetworkInfo ni = mCm.getNetworkInfo(network);
+ assertNotNull("Network returned from getActiveNetwork was invalid", ni);
+
+ // Similar to testGetActiveNetworkInfo above.
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType()));
+ assertTrue(ni.getState() == State.CONNECTED);
+ }
+
+ @Test
+ public void testGetNetworkInfo() {
+ for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE+1; type++) {
+ if (shouldBeSupported(type)) {
+ NetworkInfo ni = mCm.getNetworkInfo(type);
+ assertTrue("Info shouldn't be null for " + type, ni != null);
+ State state = ni.getState();
+ assertTrue("Bad state for " + type, State.UNKNOWN.ordinal() >= state.ordinal()
+ && state.ordinal() >= State.CONNECTING.ordinal());
+ DetailedState ds = ni.getDetailedState();
+ assertTrue("Bad detailed state for " + type,
+ DetailedState.FAILED.ordinal() >= ds.ordinal()
+ && ds.ordinal() >= DetailedState.IDLE.ordinal());
+ } else {
+ assertNull("Info should be null for " + type, mCm.getNetworkInfo(type));
+ }
+ }
+ }
+
+ @Test
+ public void testGetAllNetworkInfo() {
+ NetworkInfo[] ni = mCm.getAllNetworkInfo();
+ assertTrue(ni.length >= MIN_NUM_NETWORK_TYPES);
+ for (int type = 0; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
+ int desiredFoundCount = (shouldBeSupported(type) ? 1 : 0);
+ int foundCount = 0;
+ for (NetworkInfo i : ni) {
+ if (i.getType() == type) foundCount++;
+ }
+ if (foundCount != desiredFoundCount) {
+ Log.e(TAG, "failure in testGetAllNetworkInfo. Dump of returned NetworkInfos:");
+ for (NetworkInfo networkInfo : ni) Log.e(TAG, " " + networkInfo);
+ }
+ assertTrue("Unexpected foundCount of " + foundCount + " for type " + type,
+ foundCount == desiredFoundCount);
+ }
+ }
+
+ private String getSubscriberIdForCellNetwork(Network cellNetwork) {
+ final NetworkCapabilities cellCaps = mCm.getNetworkCapabilities(cellNetwork);
+ final NetworkSpecifier specifier = cellCaps.getNetworkSpecifier();
+ assertTrue(specifier instanceof TelephonyNetworkSpecifier);
+ // Get subscription from Telephony network specifier.
+ final int subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+ assertNotEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, subId);
+
+ // Get subscriber Id from telephony manager.
+ final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ return runWithShellPermissionIdentity(() -> tm.getSubscriberId(subId),
+ android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+ }
+
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testGetAllNetworkStateSnapshots()
+ throws InterruptedException {
+ // Make sure cell is active to retrieve IMSI for verification in later step.
+ final Network cellNetwork = mCtsNetUtils.connectToCell();
+ final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork);
+ assertFalse(TextUtils.isEmpty(subscriberId));
+
+ // Verify the API cannot be called without proper permission.
+ assertThrows(SecurityException.class, () -> mCm.getAllNetworkStateSnapshots());
+
+ // Get all networks, verify the result of getAllNetworkStateSnapshots matches the result
+ // got from other APIs.
+ final Network[] networks = mCm.getAllNetworks();
+ assertGreaterOrEqual(networks.length, 1);
+ final List<NetworkStateSnapshot> snapshots = runWithShellPermissionIdentity(
+ () -> mCm.getAllNetworkStateSnapshots(), NETWORK_SETTINGS);
+ assertEquals(networks.length, snapshots.size());
+ for (final Network network : networks) {
+ // Can't use a lambda because it will cause the test to crash on R with
+ // NoClassDefFoundError.
+ NetworkStateSnapshot snapshot = null;
+ for (NetworkStateSnapshot item : snapshots) {
+ if (item.getNetwork().equals(network)) {
+ snapshot = item;
+ break;
+ }
+ }
+ assertNotNull(snapshot);
+ final NetworkCapabilities caps =
+ Objects.requireNonNull(mCm.getNetworkCapabilities(network));
+ // Redact specifier of the capabilities of the snapshot before comparing since
+ // the result returned from getNetworkCapabilities always get redacted.
+ final NetworkSpecifier redactedSnapshotCapSpecifier =
+ snapshot.getNetworkCapabilities().getNetworkSpecifier().redact();
+ assertEquals("", caps.describeImmutableDifferences(
+ snapshot.getNetworkCapabilities()
+ .setNetworkSpecifier(redactedSnapshotCapSpecifier)));
+ assertEquals(mCm.getLinkProperties(network), snapshot.getLinkProperties());
+ assertEquals(mCm.getNetworkInfo(network).getType(), snapshot.getLegacyType());
+
+ if (network.equals(cellNetwork)) {
+ assertEquals(subscriberId, snapshot.getSubscriberId());
+ }
+ }
+ }
+
+ /**
+ * Tests that connections can be opened on WiFi and cellphone networks,
+ * and that they are made from different IP addresses.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Virtual devices use a single internet connection for all networks")
+ public void testOpenConnection() throws Exception {
+ boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
+ && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
+ if (!canRunTest) {
+ Log.i(TAG,"testOpenConnection cannot execute unless device supports both WiFi "
+ + "and a cellular connection");
+ return;
+ }
+
+ Network wifiNetwork = mCtsNetUtils.connectToWifi();
+ Network cellNetwork = mCtsNetUtils.connectToCell();
+ // This server returns the requestor's IP address as the response body.
+ URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text");
+ String wifiAddressString = httpGet(wifiNetwork, url);
+ String cellAddressString = httpGet(cellNetwork, url);
+
+ assertFalse(String.format("Same address '%s' on two different networks (%s, %s)",
+ wifiAddressString, wifiNetwork, cellNetwork),
+ wifiAddressString.equals(cellAddressString));
+
+ // Verify that the IP addresses that the requests appeared to come from are actually on the
+ // respective networks.
+ assertOnNetwork(wifiAddressString, wifiNetwork);
+ assertOnNetwork(cellAddressString, cellNetwork);
+
+ assertFalse("Unexpectedly equal: " + wifiNetwork, wifiNetwork.equals(cellNetwork));
+ }
+
+ /**
+ * Performs a HTTP GET to the specified URL on the specified Network, and returns
+ * the response body decoded as UTF-8.
+ */
+ private static String httpGet(Network network, URL httpUrl) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) network.openConnection(httpUrl);
+ try {
+ InputStream inputStream = connection.getInputStream();
+ return Streams.readFully(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ private void assertOnNetwork(String adressString, Network network) throws UnknownHostException {
+ InetAddress address = InetAddress.getByName(adressString);
+ LinkProperties linkProperties = mCm.getLinkProperties(network);
+ // To make sure that the request went out on the right network, check that
+ // the IP address seen by the server is assigned to the expected network.
+ // We can only do this for IPv6 addresses, because in IPv4 we will likely
+ // have a private IPv4 address, and that won't match what the server sees.
+ if (address instanceof Inet6Address) {
+ assertContains(linkProperties.getAddresses(), address);
+ }
+ }
+
+ private static<T> void assertContains(Collection<T> collection, T element) {
+ assertTrue(element + " not found in " + collection, collection.contains(element));
+ }
+
+ private void assertStartUsingNetworkFeatureUnsupported(int networkType, String feature) {
+ try {
+ mCm.startUsingNetworkFeature(networkType, feature);
+ fail("startUsingNetworkFeature is no longer supported in the current API version");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ private void assertStopUsingNetworkFeatureUnsupported(int networkType, String feature) {
+ try {
+ mCm.startUsingNetworkFeature(networkType, feature);
+ fail("stopUsingNetworkFeature is no longer supported in the current API version");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ private void assertRequestRouteToHostUnsupported(int networkType, int hostAddress) {
+ try {
+ mCm.requestRouteToHost(networkType, hostAddress);
+ fail("requestRouteToHost is no longer supported in the current API version");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ @Test
+ public void testStartUsingNetworkFeature() {
+
+ final String invalidateFeature = "invalidateFeature";
+ final String mmsFeature = "enableMMS";
+
+ assertStartUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature);
+ assertStopUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature);
+ assertStartUsingNetworkFeatureUnsupported(TYPE_WIFI, mmsFeature);
+ }
+
+ private boolean shouldEthernetBeSupported() {
+ // Instant mode apps aren't allowed to query the Ethernet service due to selinux policies.
+ // When in instant mode, don't fail if the Ethernet service is available. Instead, rely on
+ // the fact that Ethernet should be supported if the device has a hardware Ethernet port, or
+ // if the device can be a USB host and thus can use USB Ethernet adapters.
+ //
+ // Note that this test this will still fail in instant mode if a device supports Ethernet
+ // via other hardware means. We are not currently aware of any such device.
+ return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) ||
+ mPackageManager.hasSystemFeature(FEATURE_ETHERNET) ||
+ mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+ }
+
+ private boolean shouldBeSupported(int networkType) {
+ return mNetworkTypes.contains(networkType)
+ || (networkType == ConnectivityManager.TYPE_VPN)
+ || (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
+ }
+
+ @Test
+ public void testIsNetworkSupported() {
+ for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
+ boolean supported = mCm.isNetworkSupported(type);
+ if (shouldBeSupported(type)) {
+ assertTrue("Network type " + type + " should be supported", supported);
+ } else {
+ assertFalse("Network type " + type + " should not be supported", supported);
+ }
+ }
+ }
+
+ @Test
+ public void testRequestRouteToHost() {
+ for (int type = -1 ; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
+ assertRequestRouteToHostUnsupported(type, HOST_ADDRESS);
+ }
+ }
+
+ @Test
+ public void testTest() {
+ mCm.getBackgroundDataSetting();
+ }
+
+ private NetworkRequest makeWifiNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .build();
+ }
+
+ private NetworkRequest makeCellNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .build();
+ }
+
+ /**
+ * Exercises both registerNetworkCallback and unregisterNetworkCallback. This checks to
+ * see if we get a callback for the TRANSPORT_WIFI transport type being available.
+ *
+ * <p>In order to test that a NetworkCallback occurs, we need some change in the network
+ * state (either a transport or capability is now available). The most straightforward is
+ * WiFi. We could add a version that uses the telephony data connection but it's not clear
+ * that it would increase test coverage by much (how many devices have 3G radio but not Wifi?).
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRegisterNetworkCallback() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
+ return;
+ }
+
+ // We will register for a WIFI network being available or lost.
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+
+ final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
+ mCm.registerDefaultNetworkCallback(defaultTrackingCallback);
+
+ final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
+ final TestNetworkCallback perUidCallback = new TestNetworkCallback();
+ final Handler h = new Handler(Looper.getMainLooper());
+ if (TestUtils.shouldTestSApis()) {
+ runWithShellPermissionIdentity(() -> {
+ mCmShim.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
+ mCmShim.registerDefaultNetworkCallbackForUid(Process.myUid(), perUidCallback, h);
+ }, NETWORK_SETTINGS);
+ }
+
+ Network wifiNetwork = null;
+
+ try {
+ mCtsNetUtils.ensureWifiConnected();
+
+ // Now we should expect to get a network callback about availability of the wifi
+ // network even if it was already connected as a state-based action when the callback
+ // is registered.
+ wifiNetwork = callback.waitForAvailable();
+ assertNotNull("Did not receive onAvailable for TRANSPORT_WIFI request",
+ wifiNetwork);
+
+ final Network defaultNetwork = defaultTrackingCallback.waitForAvailable();
+ assertNotNull("Did not receive onAvailable on default network callback",
+ defaultNetwork);
+
+ if (TestUtils.shouldTestSApis()) {
+ assertNotNull("Did not receive onAvailable on system default network callback",
+ systemDefaultCallback.waitForAvailable());
+ final Network perUidNetwork = perUidCallback.waitForAvailable();
+ assertNotNull("Did not receive onAvailable on per-UID default network callback",
+ perUidNetwork);
+ assertEquals(defaultNetwork, perUidNetwork);
+ }
+
+ } catch (InterruptedException e) {
+ fail("Broadcast receiver or NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ mCm.unregisterNetworkCallback(defaultTrackingCallback);
+ if (TestUtils.shouldTestSApis()) {
+ mCm.unregisterNetworkCallback(systemDefaultCallback);
+ mCm.unregisterNetworkCallback(perUidCallback);
+ }
+ }
+ }
+
+ /**
+ * Tests both registerNetworkCallback and unregisterNetworkCallback similarly to
+ * {@link #testRegisterNetworkCallback} except that a {@code PendingIntent} is used instead
+ * of a {@code NetworkCallback}.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRegisterNetworkCallback_withPendingIntent() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
+ return;
+ }
+
+ // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
+ // action, NETWORK_CALLBACK_ACTION.
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(NETWORK_CALLBACK_ACTION);
+
+ final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
+ mContext.registerReceiver(receiver, filter);
+
+ // Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
+ final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
+ .setPackage(mContext.getPackageName());
+ // While ConnectivityService would put extra info such as network or request id before
+ // broadcasting the inner intent. The MUTABLE flag needs to be added accordingly.
+ // TODO: replace with PendingIntent.FLAG_MUTABLE when this code compiles against S+ or
+ // shims.
+ final int pendingIntentFlagMutable = 1 << 25;
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /*requestCode*/,
+ intent, PendingIntent.FLAG_CANCEL_CURRENT | pendingIntentFlagMutable);
+
+ // We will register for a WIFI network being available or lost.
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent);
+
+ try {
+ mCtsNetUtils.ensureWifiConnected();
+
+ // Now we expect to get the Intent delivered notifying of the availability of the wifi
+ // network even if it was already connected as a state-based action when the callback
+ // is registered.
+ assertTrue("Did not receive expected Intent " + intent + " for TRANSPORT_WIFI",
+ receiver.waitForState());
+ } catch (InterruptedException e) {
+ fail("Broadcast receiver or NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(pendingIntent);
+ pendingIntent.cancel();
+ mContext.unregisterReceiver(receiver);
+ }
+ }
+
+ /**
+ * Exercises the requestNetwork with NetworkCallback API. This checks to
+ * see if we get a callback for an INTERNET request.
+ */
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ @Test
+ public void testRequestNetworkCallback() {
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.requestNetwork(new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build(), callback);
+
+ try {
+ // Wait to get callback for availability of internet
+ Network internetNetwork = callback.waitForAvailable();
+ assertNotNull("Did not receive NetworkCallback#onAvailable for INTERNET",
+ internetNetwork);
+ } catch (InterruptedException e) {
+ fail("NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ /**
+ * Exercises the requestNetwork with NetworkCallback API with timeout - expected to
+ * fail. Use WIFI and switch Wi-Fi off.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRequestNetworkCallback_onUnavailable() {
+ final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.ensureWifiDisconnected(null);
+ }
+
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.requestNetwork(new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build(), callback, 100);
+
+ try {
+ // Wait to get callback for unavailability of requested network
+ assertTrue("Did not receive NetworkCallback#onUnavailable",
+ callback.waitForUnavailable());
+ } catch (InterruptedException e) {
+ fail("NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.connectToWifi();
+ }
+ }
+ }
+
+ private InetAddress getFirstV4Address(Network network) {
+ LinkProperties linkProperties = mCm.getLinkProperties(network);
+ for (InetAddress address : linkProperties.getAddresses()) {
+ if (address instanceof Inet4Address) {
+ return address;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks that enabling/disabling wifi causes CONNECTIVITY_ACTION broadcasts.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testToggleWifiConnectivityAction() {
+ // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
+ // CONNECTIVITY_ACTION broadcasts.
+ mCtsNetUtils.toggleWifi();
+ }
+
+ /** Verify restricted networks cannot be requested. */
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ @Test
+ public void testRestrictedNetworks() {
+ // Verify we can request unrestricted networks:
+ NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ NetworkCallback callback = new NetworkCallback();
+ mCm.requestNetwork(request, callback);
+ mCm.unregisterNetworkCallback(callback);
+ // Verify we cannot request restricted networks:
+ request = new NetworkRequest.Builder().addCapability(NET_CAPABILITY_IMS).build();
+ callback = new NetworkCallback();
+ try {
+ mCm.requestNetwork(request, callback);
+ fail("No exception thrown when restricted network requested.");
+ } catch (SecurityException expected) {}
+ }
+
+ // Returns "true", "false" or "none"
+ private String getWifiMeteredStatus(String ssid) throws Exception {
+ // Interestingly giving the SSID as an argument to list wifi-networks
+ // only works iff the network in question has the "false" policy.
+ // Also unfortunately runShellCommand does not pass the command to the interpreter
+ // so it's not possible to | grep the ssid.
+ final String command = "cmd netpolicy list wifi-networks";
+ final String policyString = runShellCommand(mInstrumentation, command);
+
+ final Matcher m = Pattern.compile("^" + ssid + ";(true|false|none)$",
+ Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString);
+ if (!m.find()) {
+ fail("Unexpected format from cmd netpolicy");
+ }
+ return m.group(1);
+ }
+
+ // metered should be "true", "false" or "none"
+ private void setWifiMeteredStatus(String ssid, String metered) throws Exception {
+ final String setCommand = "cmd netpolicy set metered-network " + ssid + " " + metered;
+ runShellCommand(mInstrumentation, setCommand);
+ assertEquals(getWifiMeteredStatus(ssid), metered);
+ }
+
+ private 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;
+ }
+ }
+
+ private void waitForActiveNetworkMetered(int targetTransportType, boolean requestedMeteredness)
+ throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final NetworkCallback networkCallback = new NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ if (!nc.hasTransport(targetTransportType)) return;
+
+ final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
+ if (metered == requestedMeteredness) {
+ 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.
+ mCm.registerDefaultNetworkCallback(networkCallback);
+ // Changing meteredness on wifi involves reconnecting, which can take several seconds
+ // (involves re-associating, DHCP...).
+ if (!latch.await(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ fail("Timed out waiting for active network metered status to change to "
+ + requestedMeteredness + " ; network = " + mCm.getActiveNetwork());
+ }
+ mCm.unregisterNetworkCallback(networkCallback);
+ }
+
+ private void assertMultipathPreferenceIsEventually(Network network, int oldValue,
+ int expectedValue) {
+ // Quick check : if oldValue == expectedValue, there is no way to guarantee the test
+ // is not flaky.
+ assertNotSame(oldValue, expectedValue);
+
+ for (int i = 0; i < NUM_TRIES_MULTIPATH_PREF_CHECK; ++i) {
+ final int actualValue = mCm.getMultipathPreference(network);
+ if (actualValue == expectedValue) {
+ return;
+ }
+ if (actualValue != oldValue) {
+ fail("Multipath preference is neither previous (" + oldValue
+ + ") nor expected (" + expectedValue + ")");
+ }
+ SystemClock.sleep(INTERVAL_MULTIPATH_PREF_CHECK_MS);
+ }
+ fail("Timed out waiting for multipath preference to change. expected = "
+ + expectedValue + " ; actual = " + mCm.getMultipathPreference(network));
+ }
+
+ private int getCurrentMeteredMultipathPreference(ContentResolver resolver) {
+ final String rawMeteredPref = Settings.Global.getString(resolver,
+ NETWORK_METERED_MULTIPATH_PREFERENCE);
+ return TextUtils.isEmpty(rawMeteredPref)
+ ? getIntResourceForName(NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME)
+ : Integer.parseInt(rawMeteredPref);
+ }
+
+ private int findNextPrefValue(ContentResolver resolver) {
+ // A bit of a nuclear hammer, but race conditions in CTS are bad. To be able to
+ // detect a correct setting value without race conditions, the next pref must
+ // be a valid value (range 0..3) that is different from the old setting of the
+ // metered preference and from the unmetered preference.
+ final int meteredPref = getCurrentMeteredMultipathPreference(resolver);
+ final int unmeteredPref = ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
+ if (0 != meteredPref && 0 != unmeteredPref) return 0;
+ if (1 != meteredPref && 1 != unmeteredPref) return 1;
+ return 2;
+ }
+
+ /**
+ * Verify that getMultipathPreference does return appropriate values
+ * for metered and unmetered networks.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testGetMultipathPreference() throws Exception {
+ final ContentResolver resolver = mContext.getContentResolver();
+ mCtsNetUtils.ensureWifiConnected();
+ final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ final String oldMeteredSetting = getWifiMeteredStatus(ssid);
+ final String oldMeteredMultipathPreference = Settings.Global.getString(
+ resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
+ try {
+ final int initialMeteredPreference = getCurrentMeteredMultipathPreference(resolver);
+ int newMeteredPreference = findNextPrefValue(resolver);
+ Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+ Integer.toString(newMeteredPreference));
+ setWifiMeteredStatus(ssid, "true");
+ waitForActiveNetworkMetered(TRANSPORT_WIFI, true);
+ // Wifi meterness changes from unmetered to metered will disconnect and reconnect since
+ // R.
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
+ assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
+ NET_CAPABILITY_NOT_METERED), false);
+ assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
+ newMeteredPreference);
+
+ final int oldMeteredPreference = newMeteredPreference;
+ newMeteredPreference = findNextPrefValue(resolver);
+ Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+ Integer.toString(newMeteredPreference));
+ assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
+ NET_CAPABILITY_NOT_METERED), false);
+ assertMultipathPreferenceIsEventually(network,
+ oldMeteredPreference, newMeteredPreference);
+
+ setWifiMeteredStatus(ssid, "false");
+ // No disconnect from unmetered to metered.
+ waitForActiveNetworkMetered(TRANSPORT_WIFI, false);
+ assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
+ NET_CAPABILITY_NOT_METERED), true);
+ assertMultipathPreferenceIsEventually(network, newMeteredPreference,
+ ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED);
+ } finally {
+ Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+ oldMeteredMultipathPreference);
+ setWifiMeteredStatus(ssid, oldMeteredSetting);
+ }
+ }
+
+ // TODO: move the following socket keep alive test to dedicated test class.
+ /**
+ * Callback used in tcp keepalive offload that allows caller to wait callback fires.
+ */
+ private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback {
+ public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
+
+ public static class CallbackValue {
+ public final CallbackType callbackType;
+ public final int error;
+
+ private CallbackValue(final CallbackType type, final int error) {
+ this.callbackType = type;
+ this.error = error;
+ }
+
+ public static class OnStartedCallback extends CallbackValue {
+ OnStartedCallback() { super(CallbackType.ON_STARTED, 0); }
+ }
+
+ public static class OnStoppedCallback extends CallbackValue {
+ OnStoppedCallback() { super(CallbackType.ON_STOPPED, 0); }
+ }
+
+ public static class OnErrorCallback extends CallbackValue {
+ OnErrorCallback(final int error) { super(CallbackType.ON_ERROR, error); }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o.getClass() == this.getClass()
+ && this.callbackType == ((CallbackValue) o).callbackType
+ && this.error == ((CallbackValue) o).error;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error);
+ }
+ }
+
+ private final LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+
+ @Override
+ public void onStarted() {
+ mCallbacks.add(new CallbackValue.OnStartedCallback());
+ }
+
+ @Override
+ public void onStopped() {
+ mCallbacks.add(new CallbackValue.OnStoppedCallback());
+ }
+
+ @Override
+ public void onError(final int error) {
+ mCallbacks.add(new CallbackValue.OnErrorCallback(error));
+ }
+
+ public CallbackValue pollCallback() {
+ try {
+ return mCallbacks.poll(KEEPALIVE_CALLBACK_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("Callback not seen after " + KEEPALIVE_CALLBACK_TIMEOUT_MS + " ms");
+ }
+ return null;
+ }
+ private void expectCallback(CallbackValue expectedCallback) {
+ final CallbackValue actualCallback = pollCallback();
+ assertEquals(expectedCallback, actualCallback);
+ }
+
+ public void expectStarted() {
+ expectCallback(new CallbackValue.OnStartedCallback());
+ }
+
+ public void expectStopped() {
+ expectCallback(new CallbackValue.OnStoppedCallback());
+ }
+
+ public void expectError(int error) {
+ expectCallback(new CallbackValue.OnErrorCallback(error));
+ }
+ }
+
+ private InetAddress getAddrByName(final String hostname, final int family) throws Exception {
+ final InetAddress[] allAddrs = InetAddress.getAllByName(hostname);
+ for (InetAddress addr : allAddrs) {
+ if (family == AF_INET && addr instanceof Inet4Address) return addr;
+
+ if (family == AF_INET6 && addr instanceof Inet6Address) return addr;
+
+ if (family == AF_UNSPEC) return addr;
+ }
+ return null;
+ }
+
+ private Socket getConnectedSocket(final Network network, final String host, final int port,
+ final int family) throws Exception {
+ final Socket s = network.getSocketFactory().createSocket();
+ try {
+ final InetAddress addr = getAddrByName(host, family);
+ if (addr == null) fail("Fail to get destination address for " + family);
+
+ final InetSocketAddress sockAddr = new InetSocketAddress(addr, port);
+ s.connect(sockAddr);
+ } catch (Exception e) {
+ s.close();
+ throw e;
+ }
+ return s;
+ }
+
+ private int getSupportedKeepalivesForNet(@NonNull Network network) throws Exception {
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+
+ // Get number of supported concurrent keepalives for testing network.
+ final int[] keepalivesPerTransport = KeepaliveUtils.getSupportedKeepalives(mContext);
+ return KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
+ keepalivesPerTransport, nc);
+ }
+
+ private static boolean isTcpKeepaliveSupportedByKernel() {
+ final String kVersionString = VintfRuntimeInfo.getKernelVersion();
+ return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
+ }
+
+ private static Pair<Integer, Integer> getVersionFromString(String version) {
+ // Only gets major and minor number of the version string.
+ final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
+ final Matcher m = versionPattern.matcher(version);
+ if (m.matches()) {
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
+ return new Pair<>(major, minor);
+ } else {
+ return new Pair<>(0, 0);
+ }
+ }
+
+ // TODO: Move to util class.
+ private static int compareMajorMinorVersion(final String s1, final String s2) {
+ final Pair<Integer, Integer> v1 = getVersionFromString(s1);
+ final Pair<Integer, Integer> v2 = getVersionFromString(s2);
+
+ if (v1.first == v2.first) {
+ return Integer.compare(v1.second, v2.second);
+ } else {
+ return Integer.compare(v1.first, v2.first);
+ }
+ }
+
+ /**
+ * Verifies that version string compare logic returns expected result for various cases.
+ * Note that only major and minor number are compared.
+ */
+ @Test
+ public void testMajorMinorVersionCompare() {
+ assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
+ assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
+ assertEquals(1, compareMajorMinorVersion("5.0", "4.8"));
+ assertEquals(1, compareMajorMinorVersion("5", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("5", "5.0"));
+ assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("4.8", "4.8"));
+ assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0"));
+ assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8"));
+ }
+
+ /**
+ * Verifies that the keepalive API cannot create any keepalive when the maximum number of
+ * keepalives is set to 0.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testKeepaliveWifiUnsupported() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testKeepaliveUnsupported cannot execute unless device"
+ + " supports WiFi");
+ return;
+ }
+
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ if (getSupportedKeepalivesForNet(network) != 0) return;
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ runWithShellPermissionIdentity(() -> {
+ assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 1, 0));
+ assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+ });
+ }
+
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testCreateTcpKeepalive() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testCreateTcpKeepalive cannot execute unless device supports WiFi");
+ return;
+ }
+
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ if (getSupportedKeepalivesForNet(network) == 0) return;
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
+ // NAT-T keepalive. If keepalive limits from resource overlay is not zero, TCP keepalive
+ // needs to be supported except if the kernel doesn't support it.
+ if (!isTcpKeepaliveSupportedByKernel()) {
+ // Verify that the callback result is expected.
+ runWithShellPermissionIdentity(() -> {
+ assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+ });
+ Log.i(TAG, "testCreateTcpKeepalive is skipped for kernel "
+ + VintfRuntimeInfo.getKernelVersion());
+ return;
+ }
+
+ final byte[] requestBytes = CtsNetUtils.HTTP_REQUEST.getBytes("UTF-8");
+ // So far only ipv4 tcp keepalive offload is supported.
+ // TODO: add test case for ipv6 tcp keepalive offload when it is supported.
+ try (Socket s = getConnectedSocket(network, TEST_HOST, HTTP_PORT, AF_INET)) {
+
+ // Should able to start keep alive offload when socket is idle.
+ final Executor executor = mContext.getMainExecutor();
+ final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+
+ mUiAutomation.adoptShellPermissionIdentity();
+ try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
+ sk.start(MIN_KEEPALIVE_INTERVAL);
+ callback.expectStarted();
+
+ // App should not able to write during keepalive offload.
+ final OutputStream out = s.getOutputStream();
+ try {
+ out.write(requestBytes);
+ fail("Should not able to write");
+ } catch (IOException e) { }
+ // App should not able to read during keepalive offload.
+ final InputStream in = s.getInputStream();
+ byte[] responseBytes = new byte[4096];
+ try {
+ in.read(responseBytes);
+ fail("Should not able to read");
+ } catch (IOException e) { }
+
+ // Stop.
+ sk.stop();
+ callback.expectStopped();
+ } finally {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ // Ensure socket is still connected.
+ assertTrue(s.isConnected());
+ assertFalse(s.isClosed());
+
+ // Let socket be not idle.
+ try {
+ final OutputStream out = s.getOutputStream();
+ out.write(requestBytes);
+ } catch (IOException e) {
+ fail("Failed to write data " + e);
+ }
+ // Make sure response data arrives.
+ final MessageQueue fdHandlerQueue = Looper.getMainLooper().getQueue();
+ final FileDescriptor fd = s.getFileDescriptor$();
+ final CountDownLatch mOnReceiveLatch = new CountDownLatch(1);
+ fdHandlerQueue.addOnFileDescriptorEventListener(fd, EVENT_INPUT, (readyFd, events) -> {
+ mOnReceiveLatch.countDown();
+ return 0; // Unregister listener.
+ });
+ if (!mOnReceiveLatch.await(2, TimeUnit.SECONDS)) {
+ fdHandlerQueue.removeOnFileDescriptorEventListener(fd);
+ fail("Timeout: no response data");
+ }
+
+ // Should get ERROR_SOCKET_NOT_IDLE because there is still data in the receive queue
+ // that has not been read.
+ mUiAutomation.adoptShellPermissionIdentity();
+ try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
+ sk.start(MIN_KEEPALIVE_INTERVAL);
+ callback.expectError(SocketKeepalive.ERROR_SOCKET_NOT_IDLE);
+ } finally {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+ }
+ }
+
+ private ArrayList<SocketKeepalive> createConcurrentKeepalivesOfType(
+ int requestCount, @NonNull TestSocketKeepaliveCallback callback,
+ Supplier<SocketKeepalive> kaFactory) {
+ final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
+
+ int remainingRetries = MAX_KEEPALIVE_RETRY_COUNT;
+
+ // Test concurrent keepalives with the given supplier.
+ while (kalist.size() < requestCount) {
+ final SocketKeepalive ka = kaFactory.get();
+ ka.start(MIN_KEEPALIVE_INTERVAL);
+ TestSocketKeepaliveCallback.CallbackValue cv = callback.pollCallback();
+ assertNotNull(cv);
+ if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_ERROR) {
+ if (kalist.size() == 0 && cv.error == SocketKeepalive.ERROR_UNSUPPORTED) {
+ // Unsupported.
+ break;
+ } else if (cv.error == SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES) {
+ // Limit reached or temporary unavailable due to stopped slot is not yet
+ // released.
+ if (remainingRetries > 0) {
+ SystemClock.sleep(INTERVAL_KEEPALIVE_RETRY_MS);
+ remainingRetries--;
+ continue;
+ }
+ break;
+ }
+ }
+ if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_STARTED) {
+ kalist.add(ka);
+ } else {
+ fail("Unexpected error when creating " + (kalist.size() + 1) + " "
+ + ka.getClass().getSimpleName() + ": " + cv);
+ }
+ }
+
+ return kalist;
+ }
+
+ private @NonNull ArrayList<SocketKeepalive> createConcurrentNattSocketKeepalives(
+ @NonNull Network network, @NonNull InetAddress srcAddr, int requestCount,
+ @NonNull TestSocketKeepaliveCallback callback) throws Exception {
+
+ final Executor executor = mContext.getMainExecutor();
+
+ // Initialize a real NaT-T socket.
+ final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
+ final UdpEncapsulationSocket nattSocket = mIpSec.openUdpEncapsulationSocket();
+ final InetAddress dstAddr = getAddrByName(TEST_HOST, AF_INET);
+ assertNotNull(srcAddr);
+ assertNotNull(dstAddr);
+
+ // Test concurrent Nat-T keepalives.
+ final ArrayList<SocketKeepalive> result = createConcurrentKeepalivesOfType(requestCount,
+ callback, () -> mCm.createSocketKeepalive(network, nattSocket,
+ srcAddr, dstAddr, executor, callback));
+
+ nattSocket.close();
+ return result;
+ }
+
+ private @NonNull ArrayList<SocketKeepalive> createConcurrentTcpSocketKeepalives(
+ @NonNull Network network, int requestCount,
+ @NonNull TestSocketKeepaliveCallback callback) {
+ final Executor executor = mContext.getMainExecutor();
+
+ // Create concurrent TCP keepalives.
+ return createConcurrentKeepalivesOfType(requestCount, callback, () -> {
+ // Assert that TCP connections can be established. The file descriptor of tcp
+ // sockets will be duplicated and kept valid in service side if the keepalives are
+ // successfully started.
+ try (Socket tcpSocket = getConnectedSocket(network, TEST_HOST, HTTP_PORT,
+ AF_INET)) {
+ return mCm.createSocketKeepalive(network, tcpSocket, executor, callback);
+ } catch (Exception e) {
+ fail("Unexpected error when creating TCP socket: " + e);
+ }
+ return null;
+ });
+ }
+
+ /**
+ * Creates concurrent keepalives until the specified counts of each type of keepalives are
+ * reached or the expected error callbacks are received for each type of keepalives.
+ *
+ * @return the total number of keepalives created.
+ */
+ private int createConcurrentSocketKeepalives(
+ @NonNull Network network, @NonNull InetAddress srcAddr, int nattCount, int tcpCount)
+ throws Exception {
+ final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
+ final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+
+ kalist.addAll(createConcurrentNattSocketKeepalives(network, srcAddr, nattCount, callback));
+ kalist.addAll(createConcurrentTcpSocketKeepalives(network, tcpCount, callback));
+
+ final int ret = kalist.size();
+
+ // Clean up.
+ for (final SocketKeepalive ka : kalist) {
+ ka.stop();
+ callback.expectStopped();
+ }
+ kalist.clear();
+
+ return ret;
+ }
+
+ /**
+ * Verifies that the concurrent keepalive slots meet the minimum requirement, and don't
+ * get leaked after iterations.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testSocketKeepaliveLimitWifi() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testSocketKeepaliveLimitWifi cannot execute unless device"
+ + " supports WiFi");
+ return;
+ }
+
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ final int supported = getSupportedKeepalivesForNet(network);
+ if (supported == 0) {
+ return;
+ }
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ runWithShellPermissionIdentity(() -> {
+ // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
+ assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);
+
+ // Verifies that Nat-T keepalives can be established.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported + 1, 0));
+ // Verifies that keepalives don't get leaked in second round.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+ 0));
+ });
+
+ // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
+ // NAT-T keepalive. Test below cases only if TCP keepalive is supported by kernel.
+ if (!isTcpKeepaliveSupportedByKernel()) return;
+
+ runWithShellPermissionIdentity(() -> {
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+ supported + 1));
+
+ // Verifies that different types can be established at the same time.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported / 2, supported - supported / 2));
+
+ // Verifies that keepalives don't get leaked in second round.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+ supported));
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported / 2, supported - supported / 2));
+ });
+ }
+
+ /**
+ * Verifies that the concurrent keepalive slots meet the minimum telephony requirement, and
+ * don't get leaked after iterations.
+ */
+ @AppModeFull(reason = "Cannot request network in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testSocketKeepaliveLimitTelephony() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
+ Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device"
+ + " supports telephony");
+ return;
+ }
+
+ final int firstSdk = SdkLevel.isAtLeastS()
+ ? Build.VERSION.DEVICE_INITIAL_SDK_INT
+ // FIRST_SDK_INT was a @TestApi field renamed to DEVICE_INITIAL_SDK_INT in S
+ : Build.VERSION.class.getField("FIRST_SDK_INT").getInt(null);
+ if (firstSdk < Build.VERSION_CODES.Q) {
+ Log.i(TAG, "testSocketKeepaliveLimitTelephony: skip test for devices launching"
+ + " before Q: " + firstSdk);
+ return;
+ }
+
+ final Network network = mCtsNetUtils.connectToCell();
+ final int supported = getSupportedKeepalivesForNet(network);
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ runWithShellPermissionIdentity(() -> {
+ // Verifies that the supported keepalive slots meet minimum requirement.
+ assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
+ // Verifies that Nat-T keepalives can be established.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported + 1, 0));
+ // Verifies that keepalives don't get leaked in second round.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+ 0));
+ });
+ }
+
+ private int getIntResourceForName(@NonNull String resName) {
+ final Resources r = mContext.getResources();
+ final int resId = r.getIdentifier(resName, "integer", "android");
+ return r.getInteger(resId);
+ }
+
+ /**
+ * Verifies that the keepalive slots are limited as customized for unprivileged requests.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testSocketKeepaliveUnprivileged() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testSocketKeepaliveUnprivileged cannot execute unless device"
+ + " supports WiFi");
+ return;
+ }
+
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ final int supported = getSupportedKeepalivesForNet(network);
+ if (supported == 0) {
+ return;
+ }
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ // Resource ID might be shifted on devices that compiled with different symbols.
+ // Thus, resolve ID at runtime is needed.
+ final int allowedUnprivilegedPerUid =
+ getIntResourceForName(KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME);
+ final int reservedPrivilegedSlots =
+ getIntResourceForName(KEEPALIVE_RESERVED_PER_SLOT_RES_NAME);
+ // Verifies that unprivileged request per uid cannot exceed the limit customized in the
+ // resource. Currently, unprivileged keepalive slots are limited to Nat-T only, this test
+ // does not apply to TCP.
+ assertGreaterOrEqual(supported, reservedPrivilegedSlots);
+ assertGreaterOrEqual(supported, allowedUnprivilegedPerUid);
+ final int expectedUnprivileged =
+ Math.min(allowedUnprivilegedPerUid, supported - reservedPrivilegedSlots);
+ assertEquals(expectedUnprivileged,
+ createConcurrentSocketKeepalives(network, srcAddr, supported + 1, 0));
+ }
+
+ private static void assertGreaterOrEqual(long greater, long lesser) {
+ assertTrue("" + greater + " expected to be greater than or equal to " + lesser,
+ greater >= lesser);
+ }
+
+ /**
+ * Verifies that apps are not allowed to access restricted networks even if they declare the
+ * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission in their manifests.
+ * See. b/144679405.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRestrictedNetworkPermission() throws Exception {
+ // Ensure that CONNECTIVITY_USE_RESTRICTED_NETWORKS isn't granted to this package.
+ final PackageInfo app = mPackageManager.getPackageInfo(mContext.getPackageName(),
+ GET_PERMISSIONS);
+ final int index = ArrayUtils.indexOf(
+ app.requestedPermissions, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+ assertTrue(index >= 0);
+ assertTrue(app.requestedPermissionsFlags[index] != PERMISSION_GRANTED);
+
+ // Ensure that NetworkUtils.queryUserAccess always returns false since this package should
+ // not have netd system permission to call this function.
+ final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+ assertFalse(NetworkUtils.queryUserAccess(Binder.getCallingUid(), wifiNetwork.netId));
+
+ // Ensure that this package cannot bind to any restricted network that's currently
+ // connected.
+ Network[] networks = mCm.getAllNetworks();
+ for (Network network : networks) {
+ NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+ if (nc != null && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ try {
+ network.bindSocket(new Socket());
+ fail("Bind to restricted network " + network + " unexpectedly succeeded");
+ } catch (IOException expected) {}
+ }
+ }
+ }
+
+ /**
+ * Verifies that apps are allowed to call setAirplaneMode if they declare
+ * NETWORK_AIRPLANE_MODE permission in their manifests.
+ * See b/145164696.
+ */
+ @AppModeFull(reason = "NETWORK_AIRPLANE_MODE permission can't be granted to instant apps")
+ @Test
+ public void testSetAirplaneMode() throws Exception{
+ final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI);
+ final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
+ // store the current state of airplane mode
+ final boolean isAirplaneModeEnabled = isAirplaneModeEnabled();
+ final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
+ final TestableNetworkCallback telephonyCb = new TestableNetworkCallback();
+ // disable airplane mode to reach a known state
+ runShellCommand("cmd connectivity airplane-mode disable");
+ // Verify that networks are available as expected if wifi or cell is supported. Continue the
+ // test if none of them are supported since test should still able to verify the permission
+ // mechanism.
+ if (supportWifi) requestAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb);
+ if (supportTelephony) requestAndWaitForAvailable(makeCellNetworkRequest(), telephonyCb);
+
+ try {
+ // Verify we cannot set Airplane Mode without correct permission:
+ try {
+ setAndVerifyAirplaneMode(true);
+ fail("SecurityException should have been thrown when setAirplaneMode was called"
+ + "without holding permission NETWORK_AIRPLANE_MODE.");
+ } catch (SecurityException expected) {}
+
+ // disable airplane mode again to reach a known state
+ runShellCommand("cmd connectivity airplane-mode disable");
+
+ // adopt shell permission which holds NETWORK_AIRPLANE_MODE
+ mUiAutomation.adoptShellPermissionIdentity();
+
+ // Verify we can enable Airplane Mode with correct permission:
+ try {
+ setAndVerifyAirplaneMode(true);
+ } catch (SecurityException e) {
+ fail("SecurityException should not have been thrown when setAirplaneMode(true) was"
+ + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
+ }
+ // Verify that the enabling airplane mode takes effect as expected to prevent flakiness
+ // caused by fast airplane mode switches. Ensure network lost before turning off
+ // airplane mode.
+ if (supportWifi) waitForLost(wifiCb);
+ if (supportTelephony) waitForLost(telephonyCb);
+
+ // Verify we can disable Airplane Mode with correct permission:
+ try {
+ setAndVerifyAirplaneMode(false);
+ } catch (SecurityException e) {
+ fail("SecurityException should not have been thrown when setAirplaneMode(false) was"
+ + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
+ }
+ // Verify that turning airplane mode off takes effect as expected.
+ if (supportWifi) waitForAvailable(wifiCb);
+ if (supportTelephony) waitForAvailable(telephonyCb);
+ } finally {
+ if (supportWifi) mCm.unregisterNetworkCallback(wifiCb);
+ if (supportTelephony) mCm.unregisterNetworkCallback(telephonyCb);
+ // Restore the previous state of airplane mode and permissions:
+ runShellCommand("cmd connectivity airplane-mode "
+ + (isAirplaneModeEnabled ? "enable" : "disable"));
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+ }
+
+ private void requestAndWaitForAvailable(@NonNull final NetworkRequest request,
+ @NonNull final TestableNetworkCallback cb) {
+ mCm.registerNetworkCallback(request, cb);
+ waitForAvailable(cb);
+ }
+
+ private void waitForAvailable(@NonNull final TestableNetworkCallback cb) {
+ cb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+ c -> c instanceof CallbackEntry.Available);
+ }
+
+ private void waitForLost(@NonNull final TestableNetworkCallback cb) {
+ cb.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
+ c -> c instanceof CallbackEntry.Lost);
+ }
+
+ private void setAndVerifyAirplaneMode(Boolean expectedResult)
+ throws Exception {
+ final CompletableFuture<Boolean> actualResult = new CompletableFuture();
+ BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // The defaultValue of getExtraBoolean should be the opposite of what is
+ // expected, thus ensuring a test failure if the extra is absent.
+ actualResult.complete(intent.getBooleanExtra("state", !expectedResult));
+ }
+ };
+ try {
+ mContext.registerReceiver(receiver,
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+ mCm.setAirplaneMode(expectedResult);
+ final String msg = "Setting Airplane Mode failed,";
+ assertEquals(msg, expectedResult, actualResult.get(AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS));
+ } finally {
+ mContext.unregisterReceiver(receiver);
+ }
+ }
+
+ private static boolean isAirplaneModeEnabled() {
+ return runShellCommand("cmd connectivity airplane-mode")
+ .trim().equals("enabled");
+ }
+
+ @Test
+ public void testGetCaptivePortalServerUrl() {
+ final String permission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q
+ ? CONNECTIVITY_INTERNAL
+ : NETWORK_SETTINGS;
+ final String url = runAsShell(permission, mCm::getCaptivePortalServerUrl);
+ assertNotNull("getCaptivePortalServerUrl must not be null", url);
+ try {
+ final URL parsedUrl = new URL(url);
+ // As per the javadoc, the URL must be HTTP
+ assertEquals("Invalid captive portal URL protocol", "http", parsedUrl.getProtocol());
+ } catch (MalformedURLException e) {
+ throw new AssertionFailedError("Captive portal server URL is invalid: " + e);
+ }
+ }
+
+ /**
+ * Verifies that apps are forbidden from getting ssid information from
+ * {@Code NetworkCapabilities} if they do not hold NETWORK_SETTINGS permission.
+ * See b/161370134.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testSsidInNetworkCapabilities() throws Exception {
+ assumeTrue("testSsidInNetworkCapabilities cannot execute unless device supports WiFi",
+ mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ assertNotNull("Ssid getting from WifiManager is null", ssid);
+ // This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
+ // in the NetworkCapabilities.
+ verifySsidFromQueriedNetworkCapabilities(network, ssid, false /* hasSsid */);
+ verifySsidFromCallbackNetworkCapabilities(ssid, false /* hasSsid */);
+ // Adopt shell permission to allow to get ssid information.
+ runWithShellPermissionIdentity(() -> {
+ verifySsidFromQueriedNetworkCapabilities(network, ssid, true /* hasSsid */);
+ verifySsidFromCallbackNetworkCapabilities(ssid, true /* hasSsid */);
+ });
+ }
+
+ private void verifySsidFromQueriedNetworkCapabilities(@NonNull Network network,
+ @NonNull String ssid, boolean hasSsid) throws Exception {
+ // Verify if ssid is contained in NetworkCapabilities queried from ConnectivityManager.
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+ assertNotNull("NetworkCapabilities of the network is null", nc);
+ assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
+ }
+
+ private void verifySsidFromCallbackNetworkCapabilities(@NonNull String ssid, boolean hasSsid)
+ throws Exception {
+ final CompletableFuture<NetworkCapabilities> foundNc = new CompletableFuture();
+ final NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ foundNc.complete(nc);
+ }
+ };
+ try {
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+ // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+ // because WiFi network should be connected.
+ final NetworkCapabilities nc =
+ foundNc.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ // Verify if ssid is contained in the NetworkCapabilities received from callback.
+ assertNotNull("NetworkCapabilities of the network is null", nc);
+ assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ /**
+ * Verify background request can only be requested when acquiring
+ * {@link android.Manifest.permission.NETWORK_SETTINGS}.
+ */
+ @AppModeFull(reason = "Instant apps cannot create test networks")
+ @Test
+ public void testRequestBackgroundNetwork() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(TestUtils.shouldTestSApis());
+
+ // Create a tun interface. Use the returned interface name as the specifier to create
+ // a test network request.
+ final TestNetworkManager tnm = runWithShellPermissionIdentity(() ->
+ mContext.getSystemService(TestNetworkManager.class),
+ android.Manifest.permission.MANAGE_TEST_NETWORKS);
+ final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() ->
+ tnm.createTunInterface(new LinkAddress[]{TEST_LINKADDR}),
+ android.Manifest.permission.MANAGE_TEST_NETWORKS,
+ android.Manifest.permission.NETWORK_SETTINGS);
+ assertNotNull(testNetworkInterface);
+
+ final NetworkRequest testRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ // Test networks do not have NOT_VPN or TRUSTED capabilities by default
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+ testNetworkInterface.getInterfaceName()))
+ .build();
+
+ // Verify background network cannot be requested without NETWORK_SETTINGS permission.
+ final TestableNetworkCallback callback = new TestableNetworkCallback();
+ final Handler handler = new Handler(Looper.getMainLooper());
+ assertThrows(SecurityException.class,
+ () -> mCmShim.requestBackgroundNetwork(testRequest, callback, handler));
+
+ Network testNetwork = null;
+ try {
+ // Request background test network via Shell identity which has NETWORK_SETTINGS
+ // permission granted.
+ runWithShellPermissionIdentity(
+ () -> mCmShim.requestBackgroundNetwork(testRequest, callback, handler),
+ new String[] { android.Manifest.permission.NETWORK_SETTINGS });
+
+ // Register the test network agent which has no foreground request associated to it.
+ // And verify it can satisfy the background network request just fired.
+ final Binder binder = new Binder();
+ runWithShellPermissionIdentity(() ->
+ tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder),
+ new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS,
+ android.Manifest.permission.NETWORK_SETTINGS });
+ waitForAvailable(callback);
+ testNetwork = callback.getLastAvailableNetwork();
+ assertNotNull(testNetwork);
+
+ // The test network that has just connected is a foreground network,
+ // non-listen requests will get available callback before it can be put into
+ // background if no foreground request can be satisfied. Thus, wait for a short
+ // period is needed to let foreground capability go away.
+ callback.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+ NETWORK_CALLBACK_TIMEOUT_MS,
+ c -> c instanceof CallbackEntry.CapabilitiesChanged
+ && !((CallbackEntry.CapabilitiesChanged) c).getCaps()
+ .hasCapability(NET_CAPABILITY_FOREGROUND));
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(testNetwork);
+ assertFalse("expected background network, but got " + nc,
+ nc.hasCapability(NET_CAPABILITY_FOREGROUND));
+ } finally {
+ final Network n = testNetwork;
+ runWithShellPermissionIdentity(() -> {
+ if (null != n) {
+ tnm.teardownTestNetwork(n);
+ callback.eventuallyExpect(CallbackEntry.LOST,
+ NETWORK_CALLBACK_TIMEOUT_MS,
+ lost -> n.equals(lost.getNetwork()));
+ }
+ testNetworkInterface.getFileDescriptor().close();
+ }, new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS });
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
+ public void expectAvailableCallbacks(Network network) {
+ super.expectAvailableCallbacks(network, false /* suspended */, true /* validated */,
+ BLOCKED_REASON_NONE, NETWORK_CALLBACK_TIMEOUT_MS);
+ }
+ public void expectBlockedStatusCallback(Network network, int blockedStatus) {
+ super.expectBlockedStatusCallback(blockedStatus, network, NETWORK_CALLBACK_TIMEOUT_MS);
+ }
+ public void onBlockedStatusChanged(Network network, int blockedReasons) {
+ getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons));
+ }
+ }
+
+ private void setRequireVpnForUids(boolean requireVpn, Collection<Range<Integer>> ranges)
+ throws Exception {
+ mCmShim.setRequireVpnForUids(requireVpn, ranges);
+ for (Range<Integer> range : ranges) {
+ if (requireVpn) {
+ mVpnRequiredUidRanges.add(range);
+ } else {
+ assertTrue(mVpnRequiredUidRanges.remove(range));
+ }
+ }
+ }
+
+ private void doTestBlockedStatusCallback() throws Exception {
+ final DetailedBlockedStatusCallback myUidCallback = new DetailedBlockedStatusCallback();
+ final DetailedBlockedStatusCallback otherUidCallback = new DetailedBlockedStatusCallback();
+
+ final int myUid = Process.myUid();
+ final int otherUid = UserHandle.getUid(5, Process.FIRST_APPLICATION_UID);
+ final Handler handler = new Handler(Looper.getMainLooper());
+ mCm.registerDefaultNetworkCallback(myUidCallback, handler);
+ mCmShim.registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, handler);
+
+ final Network defaultNetwork = mCm.getActiveNetwork();
+ final List<DetailedBlockedStatusCallback> allCallbacks =
+ List.of(myUidCallback, otherUidCallback);
+ for (DetailedBlockedStatusCallback callback : allCallbacks) {
+ callback.expectAvailableCallbacks(defaultNetwork);
+ }
+
+ final Range<Integer> myUidRange = new Range<>(myUid, myUid);
+ final Range<Integer> otherUidRange = new Range<>(otherUid, otherUid);
+
+ setRequireVpnForUids(true, List.of(myUidRange));
+ myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN);
+ otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+
+ setRequireVpnForUids(true, List.of(myUidRange, otherUidRange));
+ myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN);
+
+ // setRequireVpnForUids does no deduplication or refcounting. Removing myUidRange does not
+ // unblock myUid because it was added to the blocked ranges twice.
+ setRequireVpnForUids(false, List.of(myUidRange));
+ myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+
+ setRequireVpnForUids(false, List.of(myUidRange, otherUidRange));
+ myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE);
+ otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE);
+
+ myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ }
+
+ @Test
+ public void testBlockedStatusCallback() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(TestUtils.shouldTestSApis());
+ runWithShellPermissionIdentity(() -> doTestBlockedStatusCallback(), NETWORK_SETTINGS);
+ }
+
+ private void doTestLegacyLockdownEnabled() throws Exception {
+ NetworkInfo info = mCm.getActiveNetworkInfo();
+ assertNotNull(info);
+ assertEquals(DetailedState.CONNECTED, info.getDetailedState());
+
+ try {
+ mCmShim.setLegacyLockdownVpnEnabled(true);
+
+ // setLegacyLockdownVpnEnabled is asynchronous and only takes effect when the
+ // ConnectivityService handler thread processes it. Ensure it has taken effect by doing
+ // something that blocks until the handler thread is idle.
+ final TestableNetworkCallback callback = new TestableNetworkCallback();
+ mCm.registerDefaultNetworkCallback(callback);
+ waitForAvailable(callback);
+ mCm.unregisterNetworkCallback(callback);
+
+ // Test one of the effects of setLegacyLockdownVpnEnabled: the fact that any NetworkInfo
+ // in state CONNECTED is degraded to CONNECTING if the legacy VPN is not connected.
+ info = mCm.getActiveNetworkInfo();
+ assertNotNull(info);
+ assertEquals(DetailedState.CONNECTING, info.getDetailedState());
+ } finally {
+ mCmShim.setLegacyLockdownVpnEnabled(false);
+ }
+ }
+
+ @Test
+ public void testLegacyLockdownEnabled() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(TestUtils.shouldTestSApis());
+ runWithShellPermissionIdentity(() -> doTestLegacyLockdownEnabled(), NETWORK_SETTINGS);
+ }
+
+ @Test
+ public void testGetCapabilityCarrierName() {
+ assumeTrue(TestUtils.shouldTestSApis());
+ assertEquals("ENTERPRISE", NetworkInformationShimImpl.newInstance()
+ .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_ENTERPRISE));
+ assertNull(NetworkInformationShimImpl.newInstance()
+ .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+ }
+
+ @Test
+ public void testSetGlobalProxy() {
+ assumeTrue(TestUtils.shouldTestSApis());
+ // Behavior is verified in gts. Verify exception thrown w/o permission.
+ assertThrows(SecurityException.class, () -> mCm.setGlobalProxy(
+ ProxyInfo.buildDirectProxy("example.com" /* host */, 8080 /* port */)));
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/CredentialsTest.java b/tests/cts/net/src/android/net/cts/CredentialsTest.java
new file mode 100644
index 0000000..91c3621
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CredentialsTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.Credentials;
+import android.test.AndroidTestCase;
+
+public class CredentialsTest extends AndroidTestCase {
+
+ public void testCredentials() {
+ // new the Credentials instance
+ // Test with zero inputs
+ Credentials cred = new Credentials(0, 0, 0);
+ assertEquals(0, cred.getGid());
+ assertEquals(0, cred.getPid());
+ assertEquals(0, cred.getUid());
+
+ // Test with big integer
+ cred = new Credentials(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+ assertEquals(Integer.MAX_VALUE, cred.getGid());
+ assertEquals(Integer.MAX_VALUE, cred.getPid());
+ assertEquals(Integer.MAX_VALUE, cred.getUid());
+
+ // Test with big negative integer
+ cred = new Credentials(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
+ assertEquals(Integer.MIN_VALUE, cred.getGid());
+ assertEquals(Integer.MIN_VALUE, cred.getPid());
+ assertEquals(Integer.MIN_VALUE, cred.getUid());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
new file mode 100644
index 0000000..4d95fbe
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -0,0 +1,763 @@
+/*
+ * 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 android.net.cts;
+
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.FLAG_EMPTY;
+import static android.net.DnsResolver.FLAG_NO_CACHE_LOOKUP;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.system.OsConstants.ETIMEDOUT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.DnsResolver;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.ParseException;
+import android.net.cts.util.CtsNetUtils;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.net.module.util.DnsPacket;
+import com.android.testutils.SkipPresubmit;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+public class DnsResolverTest extends AndroidTestCase {
+ private static final String TAG = "DnsResolverTest";
+ private static final char[] HEX_CHARS = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+ static final String TEST_DOMAIN = "www.google.com";
+ static final String TEST_NX_DOMAIN = "test1-nx.metric.gstatic.com";
+ static final String INVALID_PRIVATE_DNS_SERVER = "invalid.google";
+ static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+ static final byte[] TEST_BLOB = new byte[]{
+ /* Header */
+ 0x55, 0x66, /* Transaction ID */
+ 0x01, 0x00, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x00, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01 /* Class */
+ };
+ static final int TIMEOUT_MS = 12_000;
+ static final int CANCEL_TIMEOUT_MS = 3_000;
+ static final int CANCEL_RETRY_TIMES = 5;
+ static final int QUERY_TIMES = 10;
+ static final int NXDOMAIN = 3;
+
+ private ContentResolver mCR;
+ private ConnectivityManager mCM;
+ private PackageManager mPackageManager;
+ private CtsNetUtils mCtsNetUtils;
+ private Executor mExecutor;
+ private Executor mExecutorInline;
+ private DnsResolver mDns;
+
+ private String mOldMode;
+ private String mOldDnsSpecifier;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ mDns = DnsResolver.getInstance();
+ mExecutor = new Handler(Looper.getMainLooper())::post;
+ mExecutorInline = (Runnable r) -> r.run();
+ mCR = getContext().getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(getContext());
+ mCtsNetUtils.storePrivateDnsSetting();
+ mPackageManager = mContext.getPackageManager();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ super.tearDown();
+ }
+
+ private static String byteArrayToHexString(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; ++i) {
+ int b = bytes[i] & 0xFF;
+ hexChars[i * 2] = HEX_CHARS[b >>> 4];
+ hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ private Network[] getTestableNetworks() {
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ mCtsNetUtils.ensureWifiConnected();
+ }
+ final ArrayList<Network> testableNetworks = new ArrayList<Network>();
+ for (Network network : mCM.getAllNetworks()) {
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ if (nc != null
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+ testableNetworks.add(network);
+ }
+ }
+
+ assertTrue(
+ "This test requires that at least one network be connected. " +
+ "Please ensure that the device is connected to a network.",
+ testableNetworks.size() >= 1);
+ // In order to test query with null network, add null as an element.
+ // Test cases which query with null network will go on default network.
+ testableNetworks.add(null);
+ return testableNetworks.toArray(new Network[0]);
+ }
+
+ static private void assertGreaterThan(String msg, int first, int second) {
+ assertTrue(msg + " Excepted " + first + " to be greater than " + second, first > second);
+ }
+
+ private static class DnsParseException extends Exception {
+ public DnsParseException(String msg) {
+ super(msg);
+ }
+ }
+
+ private static class DnsAnswer extends DnsPacket {
+ DnsAnswer(@NonNull byte[] data) throws DnsParseException {
+ super(data);
+
+ // Check QR field.(query (0), or a response (1)).
+ if ((mHeader.flags & (1 << 15)) == 0) {
+ throw new DnsParseException("Not an answer packet");
+ }
+ }
+
+ int getRcode() {
+ return mHeader.rcode;
+ }
+
+ int getANCount() {
+ return mHeader.getRecordCount(ANSECTION);
+ }
+
+ int getQDCount() {
+ return mHeader.getRecordCount(QDSECTION);
+ }
+ }
+
+ /**
+ * A query callback that ensures that the query is cancelled and that onAnswer is never
+ * called. If the query succeeds before it is cancelled, needRetry will return true so the
+ * test can retry.
+ */
+ class VerifyCancelCallback implements DnsResolver.Callback<byte[]> {
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final String mMsg;
+ private final CancellationSignal mCancelSignal;
+ private int mRcode;
+ private DnsAnswer mDnsAnswer;
+ private String mErrorMsg = null;
+
+ VerifyCancelCallback(@NonNull String msg, @Nullable CancellationSignal cancel) {
+ mMsg = msg;
+ mCancelSignal = cancel;
+ }
+
+ VerifyCancelCallback(@NonNull String msg) {
+ this(msg, null);
+ }
+
+ public boolean waitForAnswer(int timeout) throws InterruptedException {
+ return mLatch.await(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ public boolean waitForAnswer() throws InterruptedException {
+ return waitForAnswer(TIMEOUT_MS);
+ }
+
+ public boolean needRetry() throws InterruptedException {
+ return mLatch.await(CANCEL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onAnswer(@NonNull byte[] answer, int rcode) {
+ if (mCancelSignal != null && mCancelSignal.isCanceled()) {
+ mErrorMsg = mMsg + " should not have returned any answers";
+ mLatch.countDown();
+ return;
+ }
+
+ mRcode = rcode;
+ try {
+ mDnsAnswer = new DnsAnswer(answer);
+ } catch (ParseException | DnsParseException e) {
+ mErrorMsg = mMsg + e.getMessage();
+ mLatch.countDown();
+ return;
+ }
+ Log.d(TAG, "Reported blob: " + byteArrayToHexString(answer));
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull DnsResolver.DnsException error) {
+ mErrorMsg = mMsg + error.getMessage();
+ mLatch.countDown();
+ }
+
+ private void assertValidAnswer() {
+ assertNull(mErrorMsg);
+ assertNotNull(mMsg + " No valid answer", mDnsAnswer);
+ assertEquals(mMsg + " Unexpected error: reported rcode" + mRcode +
+ " blob's rcode " + mDnsAnswer.getRcode(), mRcode, mDnsAnswer.getRcode());
+ }
+
+ public void assertHasAnswer() {
+ assertValidAnswer();
+ // Check rcode field.(0, No error condition).
+ assertEquals(mMsg + " Response error, rcode: " + mRcode, mRcode, 0);
+ // Check answer counts.
+ assertGreaterThan(mMsg + " No answer found", mDnsAnswer.getANCount(), 0);
+ // Check question counts.
+ assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0);
+ }
+
+ public void assertNXDomain() {
+ assertValidAnswer();
+ // Check rcode field.(3, NXDomain).
+ assertEquals(mMsg + " Unexpected rcode: " + mRcode, mRcode, NXDOMAIN);
+ // Check answer counts. Expect 0 answer.
+ assertEquals(mMsg + " Not an empty answer", mDnsAnswer.getANCount(), 0);
+ // Check question counts.
+ assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0);
+ }
+
+ public void assertEmptyAnswer() {
+ assertValidAnswer();
+ // Check rcode field.(0, No error condition).
+ assertEquals(mMsg + " Response error, rcode: " + mRcode, mRcode, 0);
+ // Check answer counts. Expect 0 answer.
+ assertEquals(mMsg + " Not an empty answer", mDnsAnswer.getANCount(), 0);
+ // Check question counts.
+ assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0);
+ }
+ }
+
+ public void testRawQuery() throws Exception {
+ doTestRawQuery(mExecutor);
+ }
+
+ public void testRawQueryInline() throws Exception {
+ doTestRawQuery(mExecutorInline);
+ }
+
+ public void testRawQueryBlob() throws Exception {
+ doTestRawQueryBlob(mExecutor);
+ }
+
+ public void testRawQueryBlobInline() throws Exception {
+ doTestRawQueryBlob(mExecutorInline);
+ }
+
+ public void testRawQueryRoot() throws Exception {
+ doTestRawQueryRoot(mExecutor);
+ }
+
+ public void testRawQueryRootInline() throws Exception {
+ doTestRawQueryRoot(mExecutorInline);
+ }
+
+ public void testRawQueryNXDomain() throws Exception {
+ doTestRawQueryNXDomain(mExecutor);
+ }
+
+ public void testRawQueryNXDomainInline() throws Exception {
+ doTestRawQueryNXDomain(mExecutorInline);
+ }
+
+ public void testRawQueryNXDomainWithPrivateDns() throws Exception {
+ doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+ }
+
+ public void testRawQueryNXDomainInlineWithPrivateDns() throws Exception {
+ doTestRawQueryNXDomainWithPrivateDns(mExecutorInline);
+ }
+
+ public void doTestRawQuery(Executor executor) throws InterruptedException {
+ final String msg = "RawQuery " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertHasAnswer();
+ }
+ }
+
+ public void doTestRawQueryBlob(Executor executor) throws InterruptedException {
+ final byte[] blob = new byte[]{
+ /* Header */
+ 0x55, 0x66, /* Transaction ID */
+ 0x01, 0x00, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x00, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01 /* Class */
+ };
+ final String msg = "RawQuery blob " + byteArrayToHexString(blob);
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, blob, FLAG_NO_CACHE_LOOKUP, executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertHasAnswer();
+ }
+ }
+
+ public void doTestRawQueryRoot(Executor executor) throws InterruptedException {
+ final String dname = "";
+ final String msg = "RawQuery empty dname(ROOT) ";
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, dname, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ // Except no answer record because the root does not have AAAA records.
+ callback.assertEmptyAnswer();
+ }
+ }
+
+ public void doTestRawQueryNXDomain(Executor executor) throws InterruptedException {
+ final String msg = "RawQuery " + TEST_NX_DOMAIN;
+
+ for (Network network : getTestableNetworks()) {
+ final NetworkCapabilities nc = (network != null)
+ ? mCM.getNetworkCapabilities(network)
+ : mCM.getNetworkCapabilities(mCM.getActiveNetwork());
+ assertNotNull("Couldn't determine NetworkCapabilities for " + network, nc);
+ // Some cellular networks configure their DNS servers never to return NXDOMAIN, so don't
+ // test NXDOMAIN on these DNS servers.
+ // b/144521720
+ if (nc.hasTransport(TRANSPORT_CELLULAR)) continue;
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNXDomain();
+ }
+ }
+
+ public void doTestRawQueryNXDomainWithPrivateDns(Executor executor)
+ throws InterruptedException {
+ final String msg = "RawQuery " + TEST_NX_DOMAIN + " with private DNS";
+ // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
+ // b/144521720
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ for (Network network : getTestableNetworks()) {
+ final Network networkForPrivateDns =
+ (network != null) ? network : mCM.getActiveNetwork();
+ assertNotNull("Can't find network to await private DNS on", networkForPrivateDns);
+ mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
+ networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER, true);
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNXDomain();
+ }
+ }
+
+ public void testRawQueryCancel() throws InterruptedException {
+ final String msg = "Test cancel RawQuery " + TEST_DOMAIN;
+ // Start a DNS query and the cancel it immediately. Use VerifyCancelCallback to expect
+ // that the query is cancelled before it succeeds. If it is not cancelled before it
+ // succeeds, retry the test until it is.
+ for (Network network : getTestableNetworks()) {
+ boolean retry = false;
+ int round = 0;
+ do {
+ if (++round > CANCEL_RETRY_TIMES) {
+ fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times");
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg, cancelSignal);
+ mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_EMPTY,
+ mExecutor, cancelSignal, callback);
+ mExecutor.execute(() -> {
+ cancelSignal.cancel();
+ latch.countDown();
+ });
+
+ retry = callback.needRetry();
+ assertTrue(msg + " query was not cancelled",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } while (retry);
+ }
+ }
+
+ public void testRawQueryBlobCancel() throws InterruptedException {
+ final String msg = "Test cancel RawQuery blob " + byteArrayToHexString(TEST_BLOB);
+ // Start a DNS query and the cancel it immediately. Use VerifyCancelCallback to expect
+ // that the query is cancelled before it succeeds. If it is not cancelled before it
+ // succeeds, retry the test until it is.
+ for (Network network : getTestableNetworks()) {
+ boolean retry = false;
+ int round = 0;
+ do {
+ if (++round > CANCEL_RETRY_TIMES) {
+ fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times");
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg, cancelSignal);
+ mDns.rawQuery(network, TEST_BLOB, FLAG_EMPTY, mExecutor, cancelSignal, callback);
+ mExecutor.execute(() -> {
+ cancelSignal.cancel();
+ latch.countDown();
+ });
+
+ retry = callback.needRetry();
+ assertTrue(msg + " cancel is not done",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } while (retry);
+ }
+ }
+
+ public void testCancelBeforeQuery() throws InterruptedException {
+ final String msg = "Test cancelled RawQuery " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ cancelSignal.cancel();
+ mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_EMPTY,
+ mExecutor, cancelSignal, callback);
+
+ assertTrue(msg + " should not return any answers",
+ !callback.waitForAnswer(CANCEL_TIMEOUT_MS));
+ }
+ }
+
+ /**
+ * A query callback for InetAddress that ensures that the query is
+ * cancelled and that onAnswer is never called. If the query succeeds
+ * before it is cancelled, needRetry will return true so the
+ * test can retry.
+ */
+ class VerifyCancelInetAddressCallback implements DnsResolver.Callback<List<InetAddress>> {
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final String mMsg;
+ private final List<InetAddress> mAnswers;
+ private final CancellationSignal mCancelSignal;
+ private String mErrorMsg = null;
+
+ VerifyCancelInetAddressCallback(@NonNull String msg, @Nullable CancellationSignal cancel) {
+ this.mMsg = msg;
+ this.mCancelSignal = cancel;
+ mAnswers = new ArrayList<>();
+ }
+
+ public boolean waitForAnswer() throws InterruptedException {
+ return mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ public boolean needRetry() throws InterruptedException {
+ return mLatch.await(CANCEL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ public boolean isAnswerEmpty() {
+ return mAnswers.isEmpty();
+ }
+
+ public boolean hasIpv6Answer() {
+ for (InetAddress answer : mAnswers) {
+ if (answer instanceof Inet6Address) return true;
+ }
+ return false;
+ }
+
+ public boolean hasIpv4Answer() {
+ for (InetAddress answer : mAnswers) {
+ if (answer instanceof Inet4Address) return true;
+ }
+ return false;
+ }
+
+ public void assertNoError() {
+ assertNull(mErrorMsg);
+ }
+
+ @Override
+ public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+ if (mCancelSignal != null && mCancelSignal.isCanceled()) {
+ mErrorMsg = mMsg + " should not have returned any answers";
+ mLatch.countDown();
+ return;
+ }
+ for (InetAddress addr : answerList) {
+ Log.d(TAG, "Reported addr: " + addr.toString());
+ }
+ mAnswers.clear();
+ mAnswers.addAll(answerList);
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull DnsResolver.DnsException error) {
+ mErrorMsg = mMsg + error.getMessage();
+ mLatch.countDown();
+ }
+ }
+
+ public void testQueryForInetAddress() throws Exception {
+ doTestQueryForInetAddress(mExecutor);
+ }
+
+ public void testQueryForInetAddressInline() throws Exception {
+ doTestQueryForInetAddress(mExecutorInline);
+ }
+
+ public void testQueryForInetAddressIpv4() throws Exception {
+ doTestQueryForInetAddressIpv4(mExecutor);
+ }
+
+ public void testQueryForInetAddressIpv4Inline() throws Exception {
+ doTestQueryForInetAddressIpv4(mExecutorInline);
+ }
+
+ public void testQueryForInetAddressIpv6() throws Exception {
+ doTestQueryForInetAddressIpv6(mExecutor);
+ }
+
+ public void testQueryForInetAddressIpv6Inline() throws Exception {
+ doTestQueryForInetAddressIpv6(mExecutorInline);
+ }
+
+ public void testContinuousQueries() throws Exception {
+ doTestContinuousQueries(mExecutor);
+ }
+
+ @SkipPresubmit(reason = "Flaky: b/159762682; add to presubmit after fixing")
+ public void testContinuousQueriesInline() throws Exception {
+ doTestContinuousQueries(mExecutorInline);
+ }
+
+ public void doTestQueryForInetAddress(Executor executor) throws InterruptedException {
+ final String msg = "Test query for InetAddress " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ mDns.query(network, TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ }
+ }
+
+ public void testQueryCancelForInetAddress() throws InterruptedException {
+ final String msg = "Test cancel query for InetAddress " + TEST_DOMAIN;
+ // Start a DNS query and the cancel it immediately. Use VerifyCancelInetAddressCallback to
+ // expect that the query is cancelled before it succeeds. If it is not cancelled before it
+ // succeeds, retry the test until it is.
+ for (Network network : getTestableNetworks()) {
+ boolean retry = false;
+ int round = 0;
+ do {
+ if (++round > CANCEL_RETRY_TIMES) {
+ fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times");
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, cancelSignal);
+ mDns.query(network, TEST_DOMAIN, FLAG_EMPTY, mExecutor, cancelSignal, callback);
+ mExecutor.execute(() -> {
+ cancelSignal.cancel();
+ latch.countDown();
+ });
+
+ retry = callback.needRetry();
+ assertTrue(msg + " query was not cancelled",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } while (retry);
+ }
+ }
+
+ public void doTestQueryForInetAddressIpv4(Executor executor) throws InterruptedException {
+ final String msg = "Test query for IPv4 InetAddress " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ mDns.query(network, TEST_DOMAIN, TYPE_A, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ assertTrue(msg + " returned Ipv6 results", !callback.hasIpv6Answer());
+ }
+ }
+
+ public void doTestQueryForInetAddressIpv6(Executor executor) throws InterruptedException {
+ final String msg = "Test query for IPv6 InetAddress " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ mDns.query(network, TEST_DOMAIN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ assertTrue(msg + " returned Ipv4 results", !callback.hasIpv4Answer());
+ }
+ }
+
+ public void testPrivateDnsBypass() throws InterruptedException {
+ final Network[] testNetworks = getTestableNetworks();
+
+ // Set an invalid private DNS server
+ mCtsNetUtils.setPrivateDnsStrictMode(INVALID_PRIVATE_DNS_SERVER);
+ final String msg = "Test PrivateDnsBypass " + TEST_DOMAIN;
+ for (Network network : testNetworks) {
+ // This test cannot be ran with null network because we need to explicitly pass a
+ // private DNS bypassable network or bind one.
+ if (network == null) continue;
+
+ // wait for private DNS setting propagating
+ mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
+ network, INVALID_PRIVATE_DNS_SERVER, false);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final DnsResolver.Callback<List<InetAddress>> errorCallback =
+ new DnsResolver.Callback<List<InetAddress>>() {
+ @Override
+ public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+ fail(msg + " should not get valid answer");
+ }
+
+ @Override
+ public void onError(@NonNull DnsResolver.DnsException error) {
+ assertEquals(DnsResolver.ERROR_SYSTEM, error.code);
+ assertEquals(ETIMEDOUT, ((ErrnoException) error.getCause()).errno);
+ latch.countDown();
+ }
+ };
+ // Private DNS strict mode with invalid DNS server is set
+ // Expect no valid answer returned but ErrnoException with ETIMEDOUT
+ mDns.query(network, TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, errorCallback);
+
+ assertTrue(msg + " invalid server round. No response after " + TIMEOUT_MS + "ms.",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ // Bypass privateDns, expect query works fine
+ mDns.query(network.getPrivateDnsBypassingCopy(),
+ TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, callback);
+
+ assertTrue(msg + " bypass private DNS round. No answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+
+ // To ensure private DNS bypass still work even if passing null network.
+ // Bind process network with a private DNS bypassable network.
+ mCM.bindProcessToNetwork(network.getPrivateDnsBypassingCopy());
+ final VerifyCancelInetAddressCallback callbackWithNullNetwork =
+ new VerifyCancelInetAddressCallback(msg + " with null network ", null);
+ mDns.query(null,
+ TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, callbackWithNullNetwork);
+
+ assertTrue(msg + " with null network bypass private DNS round. No answer after " +
+ TIMEOUT_MS + "ms.", callbackWithNullNetwork.waitForAnswer());
+ callbackWithNullNetwork.assertNoError();
+ assertTrue(msg + " with null network returned 0 results",
+ !callbackWithNullNetwork.isAnswerEmpty());
+
+ // Reset process network to default.
+ mCM.bindProcessToNetwork(null);
+ }
+ }
+
+ 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);
+ mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A,
+ FLAG_NO_CACHE_LOOKUP, executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ assertTrue(msg + " returned " + (queryV6 ? "Ipv4" : "Ipv6") + " results",
+ queryV6 ? !callback.hasIpv4Answer() : !callback.hasIpv6Answer());
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
new file mode 100644
index 0000000..fde27e9
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.testutils.SkipPresubmit;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class DnsTest extends AndroidTestCase {
+
+ static {
+ System.loadLibrary("nativedns_jni");
+ }
+
+ private static final boolean DBG = false;
+ private static final String TAG = "DnsTest";
+ private static final String PROXY_NETWORK_TYPE = "PROXY";
+
+ private ConnectivityManager mCm;
+
+ public void setUp() {
+ mCm = getContext().getSystemService(ConnectivityManager.class);
+ }
+
+ /**
+ * @return true on success
+ */
+ private static native boolean testNativeDns();
+
+ /**
+ * Verify:
+ * DNS works - forwards and backwards, giving ipv4 and ipv6
+ * Test that DNS work on v4 and v6 networks
+ * Test Native dns calls (4)
+ * Todo:
+ * Cache is flushed when we change networks
+ * have per-network caches
+ * No cache when there's no network
+ * Perf - measure size of first and second tier caches and their effect
+ * Assert requires network permission
+ */
+ @SkipPresubmit(reason = "IPv6 support may be missing on presubmit virtual hardware")
+ public void testDnsWorks() throws Exception {
+ ensureIpv6Connectivity();
+
+ InetAddress addrs[] = {};
+ try {
+ addrs = InetAddress.getAllByName("www.google.com");
+ } catch (UnknownHostException e) {}
+ assertTrue("[RERUN] DNS could not resolve www.google.com. Check internet connection",
+ addrs.length != 0);
+ boolean foundV4 = false, foundV6 = false;
+ for (InetAddress addr : addrs) {
+ if (addr instanceof Inet4Address) foundV4 = true;
+ else if (addr instanceof Inet6Address) foundV6 = true;
+ if (DBG) Log.e(TAG, "www.google.com gave " + addr.toString());
+ }
+
+ // We should have at least one of the addresses to connect!
+ assertTrue("www.google.com must have IPv4 and/or IPv6 address", foundV4 || foundV6);
+
+ // 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)
+ && activeNetworkInfoIsProxy()) {
+ Log.i(TAG, "Skipping test because the active network type name is PROXY.");
+ return;
+ }
+
+ // Clear test state so we don't get confused with the previous results.
+ addrs = new InetAddress[0];
+ foundV4 = foundV6 = false;
+ try {
+ addrs = InetAddress.getAllByName("ipv6.google.com");
+ } catch (UnknownHostException e) {}
+ String msg =
+ "[RERUN] DNS could not resolve ipv6.google.com, check the network supports IPv6. lp=" +
+ mCm.getActiveLinkProperties();
+ assertTrue(msg, addrs.length != 0);
+ for (InetAddress addr : addrs) {
+ msg = "[RERUN] ipv6.google.com returned IPv4 address: " + addr.getHostAddress() +
+ ", check your network's DNS server. lp=" + mCm.getActiveLinkProperties();
+ assertFalse (msg, addr instanceof Inet4Address);
+ foundV6 |= (addr instanceof Inet6Address);
+ if (DBG) Log.e(TAG, "ipv6.google.com gave " + addr.toString());
+ }
+
+ assertTrue(foundV6);
+
+ assertTrue(testNativeDns());
+ }
+
+ private static final String[] URLS = { "www.google.com", "ipv6.google.com", "www.yahoo.com",
+ "facebook.com", "youtube.com", "blogspot.com", "baidu.com", "wikipedia.org",
+// live.com fails rev lookup.
+ "twitter.com", "qq.com", "msn.com", "yahoo.co.jp", "linkedin.com",
+ "taobao.com", "google.co.in", "sina.com.cn", "amazon.com", "wordpress.com",
+ "google.co.uk", "ebay.com", "yandex.ru", "163.com", "google.co.jp", "google.fr",
+ "microsoft.com", "paypal.com", "google.com.br", "flickr.com",
+ "mail.ru", "craigslist.org", "fc2.com", "google.it",
+// "apple.com", fails rev lookup
+ "google.es",
+ "imdb.com", "google.ru", "soho.com", "bbc.co.uk", "vkontakte.ru", "ask.com",
+ "tumblr.com", "weibo.com", "go.com", "xvideos.com", "livejasmin.com", "cnn.com",
+ "youku.com", "blogspot.com", "soso.com", "google.ca", "aol.com", "tudou.com",
+ "xhamster.com", "megaupload.com", "ifeng.com", "zedo.com", "mediafire.com", "ameblo.jp",
+ "pornhub.com", "google.co.id", "godaddy.com", "adobe.com", "rakuten.co.jp", "about.com",
+ "espn.go.com", "4shared.com", "alibaba.com","ebay.de", "yieldmanager.com",
+ "wordpress.org", "livejournal.com", "google.com.tr", "google.com.mx", "renren.com",
+ "livedoor.com", "google.com.au", "youporn.com", "uol.com.br", "cnet.com", "conduit.com",
+ "google.pl", "myspace.com", "nytimes.com", "ebay.co.uk", "chinaz.com", "hao123.com",
+ "thepiratebay.org", "doubleclick.com", "alipay.com", "netflix.com", "cnzz.com",
+ "huffingtonpost.com", "twitpic.com", "weather.com", "babylon.com", "amazon.de",
+ "dailymotion.com", "orkut.com", "orkut.com.br", "google.com.sa", "odnoklassniki.ru",
+ "amazon.co.jp", "google.nl", "goo.ne.jp", "stumbleupon.com", "tube8.com", "tmall.com",
+ "imgur.com", "globo.com", "secureserver.net", "fileserve.com", "tianya.cn", "badoo.com",
+ "ehow.com", "photobucket.com", "imageshack.us", "xnxx.com", "deviantart.com",
+ "filestube.com", "addthis.com", "douban.com", "vimeo.com", "sogou.com",
+ "stackoverflow.com", "reddit.com", "dailymail.co.uk", "redtube.com", "megavideo.com",
+ "taringa.net", "pengyou.com", "amazon.co.uk", "fbcdn.net", "aweber.com", "spiegel.de",
+ "rapidshare.com", "mixi.jp", "360buy.com", "google.cn", "digg.com", "answers.com",
+ "bit.ly", "indiatimes.com", "skype.com", "yfrog.com", "optmd.com", "google.com.eg",
+ "google.com.pk", "58.com", "hotfile.com", "google.co.th",
+ "bankofamerica.com", "sourceforge.net", "maktoob.com", "warriorforum.com", "rediff.com",
+ "google.co.za", "56.com", "torrentz.eu", "clicksor.com", "avg.com",
+ "download.com", "ku6.com", "statcounter.com", "foxnews.com", "google.com.ar",
+ "nicovideo.jp", "reference.com", "liveinternet.ru", "ucoz.ru", "xinhuanet.com",
+ "xtendmedia.com", "naver.com", "youjizz.com", "domaintools.com", "sparkstudios.com",
+ "rambler.ru", "scribd.com", "kaixin001.com", "mashable.com", "adultfirendfinder.com",
+ "files.wordpress.com", "guardian.co.uk", "bild.de", "yelp.com", "wikimedia.org",
+ "chase.com", "onet.pl", "ameba.jp", "pconline.com.cn", "free.fr", "etsy.com",
+ "typepad.com", "youdao.com", "megaclick.com", "digitalpoint.com", "blogfa.com",
+ "salesforce.com", "adf.ly", "ganji.com", "wikia.com", "archive.org", "terra.com.br",
+ "w3schools.com", "ezinearticles.com", "wjs.com", "google.com.my", "clickbank.com",
+ "squidoo.com", "hulu.com", "repubblica.it", "google.be", "allegro.pl", "comcast.net",
+ "narod.ru", "zol.com.cn", "orange.fr", "soufun.com", "hatena.ne.jp", "google.gr",
+ "in.com", "techcrunch.com", "orkut.co.in", "xunlei.com",
+ "reuters.com", "google.com.vn", "hostgator.com", "kaskus.us", "espncricinfo.com",
+ "hootsuite.com", "qiyi.com", "gmx.net", "xing.com", "php.net", "soku.com", "web.de",
+ "libero.it", "groupon.com", "51.la", "slideshare.net", "booking.com", "seesaa.net",
+ "126.com", "telegraph.co.uk", "wretch.cc", "twimg.com", "rutracker.org", "angege.com",
+ "nba.com", "dell.com", "leboncoin.fr", "people.com", "google.com.tw", "walmart.com",
+ "daum.net", "2ch.net", "constantcontact.com", "nifty.com", "mywebsearch.com",
+ "tripadvisor.com", "google.se", "paipai.com", "google.com.ua", "ning.com", "hp.com",
+ "google.at", "joomla.org", "icio.us", "hudong.com", "csdn.net", "getfirebug.com",
+ "ups.com", "cj.com", "google.ch", "camzap.com", "wordreference.com", "tagged.com",
+ "wp.pl", "mozilla.com", "google.ru", "usps.com", "china.com", "themeforest.net",
+ "search-results.com", "tribalfusion.com", "thefreedictionary.com", "isohunt.com",
+ "linkwithin.com", "cam4.com", "plentyoffish.com", "wellsfargo.com", "metacafe.com",
+ "depositfiles.com", "freelancer.com", "opendns.com", "homeway.com", "engadget.com",
+ "10086.cn", "360.cn", "marca.com", "dropbox.com", "ign.com", "match.com", "google.pt",
+ "facemoods.com", "hardsextube.com", "google.com.ph", "lockerz.com", "istockphoto.com",
+ "partypoker.com", "netlog.com", "outbrain.com", "elpais.com", "fiverr.com",
+ "biglobe.ne.jp", "corriere.it", "love21cn.com", "yesky.com", "spankwire.com",
+ "ig.com.br", "imagevenue.com", "hubpages.com", "google.co.ve"};
+
+// TODO - this works, but is slow and cts doesn't do anything with the result.
+// Maybe require a min performance, a min cache size (detectable) and/or move
+// to perf testing
+ private static final int LOOKUP_COUNT_GOAL = URLS.length;
+ public void skiptestDnsPerf() {
+ ArrayList<String> results = new ArrayList<String>();
+ int failures = 0;
+ try {
+ for (int numberOfUrls = URLS.length; numberOfUrls > 0; numberOfUrls--) {
+ failures = 0;
+ int iterationLimit = LOOKUP_COUNT_GOAL / numberOfUrls;
+ long startTime = SystemClock.elapsedRealtimeNanos();
+ for (int iteration = 0; iteration < iterationLimit; iteration++) {
+ for (int urlIndex = 0; urlIndex < numberOfUrls; urlIndex++) {
+ try {
+ InetAddress addr = InetAddress.getByName(URLS[urlIndex]);
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "failed first lookup of " + URLS[urlIndex]);
+ failures++;
+ try {
+ InetAddress addr = InetAddress.getByName(URLS[urlIndex]);
+ } catch (UnknownHostException ee) {
+ failures++;
+ Log.e(TAG, "failed SECOND lookup of " + URLS[urlIndex]);
+ }
+ }
+ }
+ }
+ long endTime = SystemClock.elapsedRealtimeNanos();
+ float nsPer = ((float)(endTime-startTime) / iterationLimit) / numberOfUrls/ 1000;
+ String thisResult = new String("getByName for " + numberOfUrls + " took " +
+ (endTime - startTime)/1000 + "(" + nsPer + ") with " +
+ failures + " failures\n");
+ Log.d(TAG, thisResult);
+ results.add(thisResult);
+ }
+ // build up a list of addresses
+ ArrayList<byte[]> addressList = new ArrayList<byte[]>();
+ for (String url : URLS) {
+ try {
+ InetAddress addr = InetAddress.getByName(url);
+ addressList.add(addr.getAddress());
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "Exception making reverseDNS list: " + e.toString());
+ }
+ }
+ for (int numberOfAddrs = addressList.size(); numberOfAddrs > 0; numberOfAddrs--) {
+ int iterationLimit = LOOKUP_COUNT_GOAL / numberOfAddrs;
+ failures = 0;
+ long startTime = SystemClock.elapsedRealtimeNanos();
+ for (int iteration = 0; iteration < iterationLimit; iteration++) {
+ for (int addrIndex = 0; addrIndex < numberOfAddrs; addrIndex++) {
+ try {
+ InetAddress addr = InetAddress.getByAddress(addressList.get(addrIndex));
+ String hostname = addr.getHostName();
+ } catch (UnknownHostException e) {
+ failures++;
+ Log.e(TAG, "Failure doing reverse DNS lookup: " + e.toString());
+ try {
+ InetAddress addr =
+ InetAddress.getByAddress(addressList.get(addrIndex));
+ String hostname = addr.getHostName();
+
+ } catch (UnknownHostException ee) {
+ failures++;
+ Log.e(TAG, "Failure doing SECOND reverse DNS lookup: " +
+ ee.toString());
+ }
+ }
+ }
+ }
+ long endTime = SystemClock.elapsedRealtimeNanos();
+ float nsPer = ((endTime-startTime) / iterationLimit) / numberOfAddrs / 1000;
+ String thisResult = new String("getHostName for " + numberOfAddrs + " took " +
+ (endTime - startTime)/1000 + "(" + nsPer + ") with " +
+ failures + " failures\n");
+ Log.d(TAG, thisResult);
+ results.add(thisResult);
+ }
+ for (String result : results) Log.d(TAG, result);
+
+ InetAddress exit = InetAddress.getByName("exitrightnow.com");
+ Log.e(TAG, " exit address= "+exit.toString());
+
+ } catch (Exception e) {
+ Log.e(TAG, "bad URL in testDnsPerf: " + e.toString());
+ }
+ }
+
+ private boolean activeNetworkInfoIsProxy() {
+ NetworkInfo info = mCm.getActiveNetworkInfo();
+ if (PROXY_NETWORK_TYPE.equals(info.getTypeName())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private void ensureIpv6Connectivity() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ final int TIMEOUT_MS = 5_000;
+
+ final NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
+ if (lp.hasGlobalIpv6Address()) {
+ latch.countDown();
+ }
+ }
+ };
+ mCm.registerDefaultNetworkCallback(callback);
+
+ String msg = "Default network did not provide IPv6 connectivity after " + TIMEOUT_MS
+ + "ms. Please connect to an IPv6-capable network. lp="
+ + mCm.getActiveLinkProperties();
+ try {
+ assertTrue(msg, latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IkeTunUtils.java b/tests/cts/net/src/android/net/cts/IkeTunUtils.java
new file mode 100644
index 0000000..fc25292
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IkeTunUtils.java
@@ -0,0 +1,188 @@
+/*
+ * 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 android.net.cts;
+
+import static android.net.cts.PacketUtils.BytePayload;
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.IpHeader;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.net.cts.PacketUtils.UdpHeader;
+import static android.net.cts.PacketUtils.getIpHeader;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import android.os.ParcelFileDescriptor;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+// TODO: Merge this with the version in the IPsec module (IKEv2 library) CTS tests.
+/** An extension of the TunUtils class with IKE-specific packet handling. */
+public class IkeTunUtils extends TunUtils {
+ private static final int PORT_LEN = 2;
+
+ private static final byte[] NON_ESP_MARKER = new byte[] {0, 0, 0, 0};
+
+ private static final int IKE_HEADER_LEN = 28;
+ private static final int IKE_SPI_LEN = 8;
+ private static final int IKE_IS_RESP_BYTE_OFFSET = 19;
+ private static final int IKE_MSG_ID_OFFSET = 20;
+ private static final int IKE_MSG_ID_LEN = 4;
+
+ public IkeTunUtils(ParcelFileDescriptor tunFd) {
+ super(tunFd);
+ }
+
+ /**
+ * Await an expected IKE request and inject an IKE response.
+ *
+ * @param respIkePkt IKE response packet without IP/UDP headers or NON ESP MARKER.
+ */
+ public byte[] awaitReqAndInjectResp(long expectedInitIkeSpi, int expectedMsgId,
+ boolean encapExpected, byte[] respIkePkt) throws Exception {
+ final byte[] request = awaitIkePacket(expectedInitIkeSpi, expectedMsgId, encapExpected);
+
+ // Build response header by flipping address and port
+ final InetAddress srcAddr = getDstAddress(request);
+ final InetAddress dstAddr = getSrcAddress(request);
+ final int srcPort = getDstPort(request);
+ final int dstPort = getSrcPort(request);
+
+ final byte[] response =
+ buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, encapExpected, respIkePkt);
+ injectPacket(response);
+ return request;
+ }
+
+ private byte[] awaitIkePacket(long expectedInitIkeSpi, int expectedMsgId, boolean expectEncap)
+ throws Exception {
+ return super.awaitPacket(pkt -> isIke(pkt, expectedInitIkeSpi, expectedMsgId, expectEncap));
+ }
+
+ private static boolean isIke(
+ byte[] pkt, long expectedInitIkeSpi, int expectedMsgId, boolean encapExpected) {
+ final int ipProtocolOffset;
+ final int ikeOffset;
+
+ if (isIpv6(pkt)) {
+ ipProtocolOffset = IP6_PROTO_OFFSET;
+ ikeOffset = IP6_HDRLEN + UDP_HDRLEN;
+ } else {
+ if (encapExpected && !hasNonEspMarkerv4(pkt)) {
+ return false;
+ }
+
+ // Use default IPv4 header length (assuming no options)
+ final int encapMarkerLen = encapExpected ? NON_ESP_MARKER.length : 0;
+ ipProtocolOffset = IP4_PROTO_OFFSET;
+ ikeOffset = IP4_HDRLEN + UDP_HDRLEN + encapMarkerLen;
+ }
+
+ return pkt[ipProtocolOffset] == IPPROTO_UDP
+ && areSpiAndMsgIdEqual(pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId);
+ }
+
+ /** Checks if the provided IPv4 packet has a UDP-encapsulation NON-ESP marker */
+ private static boolean hasNonEspMarkerv4(byte[] ipv4Pkt) {
+ final int nonEspMarkerOffset = IP4_HDRLEN + UDP_HDRLEN;
+ if (ipv4Pkt.length < nonEspMarkerOffset + NON_ESP_MARKER.length) {
+ return false;
+ }
+
+ final byte[] nonEspMarker = Arrays.copyOfRange(
+ ipv4Pkt, nonEspMarkerOffset, nonEspMarkerOffset + NON_ESP_MARKER.length);
+ return Arrays.equals(NON_ESP_MARKER, nonEspMarker);
+ }
+
+ private static boolean areSpiAndMsgIdEqual(
+ byte[] pkt, int ikeOffset, long expectedIkeInitSpi, int expectedMsgId) {
+ if (pkt.length <= ikeOffset + IKE_HEADER_LEN) {
+ return false;
+ }
+
+ final ByteBuffer buffer = ByteBuffer.wrap(pkt);
+ final long spi = buffer.getLong(ikeOffset);
+ final int msgId = buffer.getInt(ikeOffset + IKE_MSG_ID_OFFSET);
+
+ return expectedIkeInitSpi == spi && expectedMsgId == msgId;
+ }
+
+ private static InetAddress getSrcAddress(byte[] pkt) throws Exception {
+ return getAddress(pkt, true);
+ }
+
+ private static InetAddress getDstAddress(byte[] pkt) throws Exception {
+ return getAddress(pkt, false);
+ }
+
+ private static InetAddress getAddress(byte[] pkt, boolean getSrcAddr) throws Exception {
+ final int ipLen = isIpv6(pkt) ? IP6_ADDR_LEN : IP4_ADDR_LEN;
+ final int srcIpOffset = isIpv6(pkt) ? IP6_ADDR_OFFSET : IP4_ADDR_OFFSET;
+ final int ipOffset = getSrcAddr ? srcIpOffset : srcIpOffset + ipLen;
+
+ if (pkt.length < ipOffset + ipLen) {
+ // Should be impossible; getAddress() is only called with a full IKE request including
+ // the IP and UDP headers.
+ throw new IllegalArgumentException("Packet was too short to contain IP address");
+ }
+
+ return InetAddress.getByAddress(Arrays.copyOfRange(pkt, ipOffset, ipOffset + ipLen));
+ }
+
+ private static int getSrcPort(byte[] pkt) throws Exception {
+ return getPort(pkt, true);
+ }
+
+ private static int getDstPort(byte[] pkt) throws Exception {
+ return getPort(pkt, false);
+ }
+
+ private static int getPort(byte[] pkt, boolean getSrcPort) {
+ final int srcPortOffset = isIpv6(pkt) ? IP6_HDRLEN : IP4_HDRLEN;
+ final int portOffset = getSrcPort ? srcPortOffset : srcPortOffset + PORT_LEN;
+
+ if (pkt.length < portOffset + PORT_LEN) {
+ // Should be impossible; getPort() is only called with a full IKE request including the
+ // IP and UDP headers.
+ throw new IllegalArgumentException("Packet was too short to contain port");
+ }
+
+ final ByteBuffer buffer = ByteBuffer.wrap(pkt);
+ return Short.toUnsignedInt(buffer.getShort(portOffset));
+ }
+
+ private static byte[] buildIkePacket(
+ InetAddress srcAddr,
+ InetAddress dstAddr,
+ int srcPort,
+ int dstPort,
+ boolean useEncap,
+ byte[] payload)
+ throws Exception {
+ // Append non-ESP marker if encap is enabled
+ if (useEncap) {
+ final ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER.length + payload.length);
+ buffer.put(NON_ESP_MARKER);
+ buffer.put(payload);
+ payload = buffer.array();
+ }
+
+ final UdpHeader udpPkt = new UdpHeader(srcPort, dstPort, new BytePayload(payload));
+ final IpHeader ipPkt = getIpHeader(udpPkt.getProtocolId(), srcAddr, dstAddr, udpPkt);
+ return ipPkt.getPacketBytes();
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
new file mode 100644
index 0000000..c6d8d65
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -0,0 +1,535 @@
+/*
+ * 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 android.net.cts;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Ikev2VpnProfile;
+import android.net.IpSecAlgorithm;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.ProxyInfo;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.VpnManager;
+import android.net.cts.util.CtsNetUtils;
+import android.os.Build;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.bouncycastle.x509.X509V1CertificateGenerator;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.security.auth.x500.X500Principal;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.Q)
+@AppModeFull(reason = "Appops state changes disallowed for instant apps (OP_ACTIVATE_PLATFORM_VPN)")
+public class Ikev2VpnTest {
+ private static final String TAG = Ikev2VpnTest.class.getSimpleName();
+
+ // Test vectors for IKE negotiation in test mode.
+ private static final String SUCCESSFUL_IKE_INIT_RESP_V4 =
+ "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000"
+ + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800"
+ + "100000b8070f159fe5141d8754ca86f72ecc28d66f514927e96cbe9eec0adb42bf2c276a0ab7"
+ + "a97fa93555f4be9218c14e7f286bb28c6b4fb13825a420f2ffc165854f200bab37d69c8963d4"
+ + "0acb831d983163aa50622fd35c182efe882cf54d6106222abcfaa597255d302f1b95ab71c142"
+ + "c279ea5839a180070bff73f9d03fab815f0d5ee2adec7e409d1e35979f8bd92ffd8aab13d1a0"
+ + "0657d816643ae767e9ae84d2ccfa2bcce1a50572be8d3748ae4863c41ae90da16271e014270f"
+ + "77edd5cd2e3299f3ab27d7203f93d770bacf816041cdcecd0f9af249033979da4369cb242dd9"
+ + "6d172e60513ff3db02de63e50eb7d7f596ada55d7946cad0af0669d1f3e2804846ab3f2a930d"
+ + "df56f7f025f25c25ada694e6231abbb87ee8cfd072c8481dc0b0f6b083fdc3bd89b080e49feb"
+ + "0288eef6fdf8a26ee2fc564a11e7385215cf2deaf2a9965638fc279c908ccdf04094988d91a2"
+ + "464b4a8c0326533aff5119ed79ecbd9d99a218b44f506a5eb09351e67da86698b4c58718db25"
+ + "d55f426fb4c76471b27a41fbce00777bc233c7f6e842e39146f466826de94f564cad8b92bfbe"
+ + "87c99c4c7973ec5f1eea8795e7da82819753aa7c4fcfdab77066c56b939330c4b0d354c23f83"
+ + "ea82fa7a64c4b108f1188379ea0eb4918ee009d804100e6bf118771b9058d42141c847d5ec37"
+ + "6e5ec591c71fc9dac01063c2bd31f9c783b28bf1182900002430f3d5de3449462b31dd28bc27"
+ + "297b6ad169bccce4f66c5399c6e0be9120166f2900001c0000400428b8df2e66f69c8584a186"
+ + "c5eac66783551d49b72900001c000040054e7a622e802d5cbfb96d5f30a6e433994370173529"
+ + "0000080000402e290000100000402f00020003000400050000000800004014";
+ private static final String SUCCESSFUL_IKE_INIT_RESP_V6 =
+ "46b8eca1e0d72a1800d9ea1babce26bf2120222000000000000002d0220000300000002c01010004030000"
+ + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800"
+ + "100000ea0e6dd9ca5930a9a45c323a41f64bfd8cdef7730f5fbff37d7c377da427f489a42aa8"
+ + "c89233380e6e925990d49de35c2cdcf63a61302c731a4b3569df1ee1bf2457e55a6751838ede"
+ + "abb75cc63ba5c9e4355e8e784f383a5efe8a44727dc14aeaf8dacc2620fb1c8875416dc07739"
+ + "7fe4decc1bd514a9c7d270cf21fd734c63a25c34b30b68686e54e8a198f37f27cb491fe27235"
+ + "fab5476b036d875ccab9a68d65fbf3006197f9bebbf94de0d3802b4fafe1d48d931ce3a1a346"
+ + "2d65bd639e9bd7fa46299650a9dbaf9b324e40b466942d91a59f41ef8042f8474c4850ed0f63"
+ + "e9238949d41cd8bbaea9aefdb65443a6405792839563aa5dc5c36b5ce8326ccf8a94d9622b85"
+ + "038d390d5fc0299e14e1f022966d4ac66515f6108ca04faec44821fe5bbf2ed4f84ff5671219"
+ + "608cb4c36b44a31ba010c9088f8d5ff943bb9ff857f74be1755f57a5783874adc57f42bb174e"
+ + "4ad3215de628707014dbcb1707bd214658118fdd7a42b3e1638b991ce5b812a667f1145be811"
+ + "685e3cd3baf9b18d062657b64c206a4d19a531c252a6a51a04aeaf42c618620cdbab65baca23"
+ + "82c57ed888422aeaacf7f1bc3fe2247ff7e7eaca218b74d7b31d02f2b0afa123f802529e7e6c"
+ + "3259d418290740ddbf55686e26998d7edcbbf895664972fed666f2f20af40503aa2af436ec6d"
+ + "4ec981ab19b9088755d94ae7a7c2066ea331d4e56e290000243fefe5555fce552d57a84e682c"
+ + "d4a6dfb3f2f94a94464d5bec3d88b88e9559642900001c00004004eb4afff764e7b79bca78b1"
+ + "3a89100d36d678ae982900001c00004005d177216a3c26f782076e12570d40bfaaa148822929"
+ + "0000080000402e290000100000402f00020003000400050000000800004014";
+ private static final String SUCCESSFUL_IKE_AUTH_RESP_V4 =
+ "46b8eca1e0d72a18b2b5d9006d47a0022e20232000000001000000e0240000c420a2500a3da4c66fa6929e"
+ + "600f36349ba0e38de14f78a3ad0416cba8c058735712a3d3f9a0a6ed36de09b5e9e02697e7c4"
+ + "2d210ac86cfbd709503cfa51e2eab8cfdc6427d136313c072968f6506a546eb5927164200592"
+ + "6e36a16ee994e63f029432a67bc7d37ca619e1bd6e1678df14853067ecf816b48b81e8746069"
+ + "406363e5aa55f13cb2afda9dbebee94256c29d630b17dd7f1ee52351f92b6e1c3d8551c513f1"
+ + "d74ac52a80b2041397e109fe0aeb3c105b0d4be0ae343a943398764281";
+ private static final String SUCCESSFUL_IKE_AUTH_RESP_V6 =
+ "46b8eca1e0d72a1800d9ea1babce26bf2e20232000000001000000f0240000d4aaf6eaa6c06b50447e6f54"
+ + "827fd8a9d9d6ac8015c1ebb3e8cb03fc6e54b49a107441f50004027cc5021600828026367f03"
+ + "bc425821cd7772ee98637361300c9b76056e874fea2bd4a17212370b291894264d8c023a01d1"
+ + "c3b691fd4b7c0b534e8c95af4c4638e2d125cb21c6267e2507cd745d72e8da109c47b9259c6c"
+ + "57a26f6bc5b337b9b9496d54bdde0333d7a32e6e1335c9ee730c3ecd607a8689aa7b0577b74f"
+ + "3bf437696a9fd5fc0aee3ed346cd9e15d1dda293df89eb388a8719388a60ca7625754de12cdb"
+ + "efe4c886c5c401";
+ private static final long IKE_INITIATOR_SPI = Long.parseLong("46B8ECA1E0D72A18", 16);
+
+ private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1");
+ private static final InetAddress LOCAL_OUTER_6 =
+ InetAddress.parseNumericAddress("2001:db8::1");
+
+ private static final int IP4_PREFIX_LEN = 32;
+ private static final int IP6_PREFIX_LEN = 128;
+
+ // TODO: Use IPv6 address when we can generate test vectors (GCE does not allow IPv6 yet).
+ private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2";
+ private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2";
+ private static final String TEST_IDENTITY = "client.cts.android.com";
+ private static final List<String> TEST_ALLOWED_ALGORITHMS =
+ Arrays.asList(IpSecAlgorithm.AUTH_CRYPT_AES_GCM);
+
+ private static final ProxyInfo TEST_PROXY_INFO =
+ ProxyInfo.buildDirectProxy("proxy.cts.android.com", 1234);
+ private static final int TEST_MTU = 1300;
+
+ private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes();
+ private static final String TEST_USER = "username";
+ private static final String TEST_PASSWORD = "pa55w0rd";
+
+ // Static state to reduce setup/teardown
+ private static final Context sContext = InstrumentationRegistry.getContext();
+ private static final ConnectivityManager sCM =
+ (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ private static final VpnManager sVpnMgr =
+ (VpnManager) sContext.getSystemService(Context.VPN_MANAGEMENT_SERVICE);
+ private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
+
+ private final X509Certificate mServerRootCa;
+ private final CertificateAndKey mUserCertKey;
+
+ public Ikev2VpnTest() throws Exception {
+ // Build certificates
+ mServerRootCa = generateRandomCertAndKeyPair().cert;
+ mUserCertKey = generateRandomCertAndKeyPair();
+ }
+
+ @After
+ public void tearDown() {
+ setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+ }
+
+ /**
+ * Sets the given appop using shell commands
+ *
+ * <p>This method must NEVER be called from within a shell permission, as it will attempt to
+ * acquire, and then drop the shell permission identity. This results in the caller losing the
+ * shell permission identity due to these calls not being reference counted.
+ */
+ public void setAppop(int appop, boolean allow) {
+ // Requires shell permission to update appops.
+ runWithShellPermissionIdentity(() -> {
+ mCtsNetUtils.setAppopPrivileged(appop, allow);
+ }, Manifest.permission.MANAGE_TEST_NETWORKS);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfileCommon(
+ Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception {
+ if (isRestrictedToTestNetworks) {
+ builder.restrictToTestNetworks();
+ }
+
+ return builder.setBypassable(true)
+ .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
+ .setProxy(TEST_PROXY_INFO)
+ .setMaxMtu(TEST_MTU)
+ .setMetered(false)
+ .build();
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfilePsk(boolean isRestrictedToTestNetworks)
+ throws Exception {
+ return buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6, isRestrictedToTestNetworks);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfilePsk(
+ String remote, boolean isRestrictedToTestNetworks) throws Exception {
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
+
+ return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
+ throws Exception {
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
+
+ return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
+ throws Exception {
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ .setAuthDigitalSignature(
+ mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
+
+ return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+ }
+
+ private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception {
+ assertEquals(TEST_SERVER_ADDR_V6, profile.getServerAddr());
+ assertEquals(TEST_IDENTITY, profile.getUserIdentity());
+ assertEquals(TEST_PROXY_INFO, profile.getProxyInfo());
+ assertEquals(TEST_ALLOWED_ALGORITHMS, profile.getAllowedAlgorithms());
+ assertTrue(profile.isBypassable());
+ assertFalse(profile.isMetered());
+ assertEquals(TEST_MTU, profile.getMaxMtu());
+ assertFalse(profile.isRestrictedToTestNetworks());
+ }
+
+ @Test
+ public void testBuildIkev2VpnProfilePsk() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+
+ checkBasicIkev2VpnProfile(profile);
+ assertArrayEquals(TEST_PSK, profile.getPresharedKey());
+
+ // Verify nothing else is set.
+ assertNull(profile.getUsername());
+ assertNull(profile.getPassword());
+ assertNull(profile.getServerRootCaCert());
+ assertNull(profile.getRsaPrivateKey());
+ assertNull(profile.getUserCert());
+ }
+
+ @Test
+ public void testBuildIkev2VpnProfileUsernamePassword() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfileUsernamePassword(false /* isRestrictedToTestNetworks */);
+
+ checkBasicIkev2VpnProfile(profile);
+ assertEquals(TEST_USER, profile.getUsername());
+ assertEquals(TEST_PASSWORD, profile.getPassword());
+ assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+ // Verify nothing else is set.
+ assertNull(profile.getPresharedKey());
+ assertNull(profile.getRsaPrivateKey());
+ assertNull(profile.getUserCert());
+ }
+
+ @Test
+ public void testBuildIkev2VpnProfileDigitalSignature() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfileDigitalSignature(false /* isRestrictedToTestNetworks */);
+
+ checkBasicIkev2VpnProfile(profile);
+ assertEquals(mUserCertKey.cert, profile.getUserCert());
+ assertEquals(mUserCertKey.key, profile.getRsaPrivateKey());
+ assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+ // Verify nothing else is set.
+ assertNull(profile.getUsername());
+ assertNull(profile.getPassword());
+ assertNull(profile.getPresharedKey());
+ }
+
+ private void verifyProvisionVpnProfile(
+ boolean hasActivateVpn, boolean hasActivatePlatformVpn, boolean expectIntent)
+ throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ setAppop(AppOpsManager.OP_ACTIVATE_VPN, hasActivateVpn);
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, hasActivatePlatformVpn);
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+ final Intent intent = sVpnMgr.provisionVpnProfile(profile);
+ assertEquals(expectIntent, intent != null);
+ }
+
+ @Test
+ public void testProvisionVpnProfileNoPreviousConsent() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(false /* hasActivateVpn */,
+ false /* hasActivatePlatformVpn */, true /* expectIntent */);
+ }
+
+ @Test
+ public void testProvisionVpnProfilePlatformVpnConsented() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(false /* hasActivateVpn */,
+ true /* hasActivatePlatformVpn */, false /* expectIntent */);
+ }
+
+ @Test
+ public void testProvisionVpnProfileVpnServiceConsented() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(true /* hasActivateVpn */,
+ false /* hasActivatePlatformVpn */, false /* expectIntent */);
+ }
+
+ @Test
+ public void testProvisionVpnProfileAllPreConsented() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(true /* hasActivateVpn */,
+ true /* hasActivatePlatformVpn */, false /* expectIntent */);
+ }
+
+ @Test
+ public void testDeleteVpnProfile() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+ assertNull(sVpnMgr.provisionVpnProfile(profile));
+
+ // Verify that deleting the profile works (even without the appop)
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+ sVpnMgr.deleteProvisionedVpnProfile();
+
+ // Test that the profile was deleted - starting it should throw an IAE.
+ try {
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
+ sVpnMgr.startProvisionedVpnProfile();
+ fail("Expected IllegalArgumentException due to missing profile");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void testStartVpnProfileNoPreviousConsent() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+
+ // Make sure the VpnProfile is not provisioned already.
+ sVpnMgr.stopProvisionedVpnProfile();
+
+ try {
+ sVpnMgr.startProvisionedVpnProfile();
+ fail("Expected SecurityException for missing consent");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6)
+ throws Exception {
+ String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
+ String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
+ String authResp = testIpv6 ? SUCCESSFUL_IKE_AUTH_RESP_V6 : SUCCESSFUL_IKE_AUTH_RESP_V4;
+ boolean hasNat = !testIpv6;
+
+ // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile.
+ mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */);
+ assertNull(sVpnMgr.provisionVpnProfile(profile));
+
+ sVpnMgr.startProvisionedVpnProfile();
+
+ // Inject IKE negotiation
+ int expectedMsgId = 0;
+ tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, false /* isEncap */,
+ HexDump.hexStringToByteArray(initResp));
+ tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, hasNat /* isEncap */,
+ HexDump.hexStringToByteArray(authResp));
+
+ // Verify the VPN network came up
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
+
+ final TestNetworkCallback cb = new TestNetworkCallback();
+ sCM.requestNetwork(nr, cb);
+ cb.waitForAvailable();
+ final Network vpnNetwork = cb.currentNetwork;
+ assertNotNull(vpnNetwork);
+
+ final NetworkCapabilities caps = sCM.getNetworkCapabilities(vpnNetwork);
+ assertTrue(caps.hasTransport(TRANSPORT_VPN));
+ assertTrue(caps.hasCapability(NET_CAPABILITY_INTERNET));
+ assertEquals(Process.myUid(), caps.getOwnerUid());
+
+ sVpnMgr.stopProvisionedVpnProfile();
+ cb.waitForLost();
+ assertEquals(vpnNetwork, cb.lastLostNetwork);
+ }
+
+ private void doTestStartStopVpnProfile(boolean testIpv6) throws Exception {
+ // Non-final; these variables ensure we clean up properly after our test if we have
+ // allocated test network resources
+ final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class);
+ TestNetworkInterface testIface = null;
+ TestNetworkCallback tunNetworkCallback = null;
+
+ try {
+ // Build underlying test network
+ testIface = tnm.createTunInterface(
+ new LinkAddress[] {
+ new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
+ new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)});
+
+ // Hold on to this callback to ensure network does not get reaped.
+ tunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(
+ testIface.getInterfaceName());
+ final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
+
+ checkStartStopVpnProfileBuildsNetworks(tunUtils, testIpv6);
+ } finally {
+ // Make sure to stop the VPN profile. This is safe to call multiple times.
+ sVpnMgr.stopProvisionedVpnProfile();
+
+ if (testIface != null) {
+ testIface.getFileDescriptor().close();
+ }
+
+ if (tunNetworkCallback != null) {
+ sCM.unregisterNetworkCallback(tunNetworkCallback);
+ }
+
+ final Network testNetwork = tunNetworkCallback.currentNetwork;
+ if (testNetwork != null) {
+ tnm.teardownTestNetwork(testNetwork);
+ }
+ }
+ }
+
+ @Test
+ public void testStartStopVpnProfileV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Requires shell permission to update appops.
+ runWithShellPermissionIdentity(() -> {
+ doTestStartStopVpnProfile(false);
+ });
+ }
+
+ @Test
+ public void testStartStopVpnProfileV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Requires shell permission to update appops.
+ runWithShellPermissionIdentity(() -> {
+ doTestStartStopVpnProfile(true);
+ });
+ }
+
+ private static class CertificateAndKey {
+ public final X509Certificate cert;
+ public final PrivateKey key;
+
+ CertificateAndKey(X509Certificate cert, PrivateKey key) {
+ this.cert = cert;
+ this.key = key;
+ }
+ }
+
+ private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
+ final Date validityBeginDate =
+ new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
+ final Date validityEndDate =
+ new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
+
+ // Generate a keypair
+ final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(512);
+ final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ final X500Principal dnName = new X500Principal("CN=test.android.com");
+ final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+ certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+ certGen.setSubjectDN(dnName);
+ certGen.setIssuerDN(dnName);
+ certGen.setNotBefore(validityBeginDate);
+ certGen.setNotAfter(validityEndDate);
+ certGen.setPublicKey(keyPair.getPublic());
+ certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+ final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
+ return new CertificateAndKey(cert, keyPair.getPrivate());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/InetAddressesTest.java b/tests/cts/net/src/android/net/cts/InetAddressesTest.java
new file mode 100644
index 0000000..7837ce9
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/InetAddressesTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts;
+
+import android.net.InetAddresses;
+import java.net.InetAddress;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(JUnitParamsRunner.class)
+public class InetAddressesTest {
+
+ public static String[][] validNumericAddressesAndStringRepresentation() {
+ return new String[][] {
+ // Regular IPv4.
+ { "1.2.3.4", "1.2.3.4" },
+
+ // Regular IPv6.
+ { "2001:4860:800d::68", "2001:4860:800d::68" },
+ { "1234:5678::9ABC:DEF0", "1234:5678::9abc:def0" },
+ { "2001:cdba:9abc:5678::", "2001:cdba:9abc:5678::" },
+ { "::2001:cdba:9abc:5678", "::2001:cdba:9abc:5678" },
+ { "64:ff9b::1.2.3.4", "64:ff9b::102:304" },
+
+ { "::9abc:5678", "::154.188.86.120" },
+
+ // Mapped IPv4
+ { "::ffff:127.0.0.1", "127.0.0.1" },
+
+ // Android does not recognize Octal (leading 0) cases: they are treated as decimal.
+ { "0177.00.00.01", "177.0.0.1" },
+
+ // Verify that examples from JavaDoc work correctly.
+ { "192.0.2.1", "192.0.2.1" },
+ { "2001:db8::1:2", "2001:db8::1:2" },
+ };
+ }
+
+ public static String[] invalidNumericAddresses() {
+ return new String[] {
+ "",
+ " ",
+ "\t",
+ "\n",
+ "1.2.3.4.",
+ "1.2.3",
+ "1.2",
+ "1",
+ "1234",
+ "0",
+ "0x1.0x2.0x3.0x4",
+ "0x7f.0x00.0x00.0x01",
+ "0256.00.00.01",
+ "fred",
+ "www.google.com",
+ // IPv6 encoded for use in URL as defined in RFC 2732
+ "[fe80::6:2222]",
+ };
+ }
+
+ @Parameters(method = "validNumericAddressesAndStringRepresentation")
+ @Test
+ public void parseNumericAddress(String address, String expectedString) {
+ InetAddress inetAddress = InetAddresses.parseNumericAddress(address);
+ assertEquals(expectedString, inetAddress.getHostAddress());
+ }
+
+ @Parameters(method = "invalidNumericAddresses")
+ @Test
+ public void test_parseNonNumericAddress(String address) {
+ try {
+ InetAddress inetAddress = InetAddresses.parseNumericAddress(address);
+ fail(String.format(
+ "Address %s is not numeric but was parsed as %s", address, inetAddress));
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains(address);
+ }
+ }
+
+ @Test
+ public void test_parseNumericAddress_null() {
+ try {
+ InetAddress inetAddress = InetAddresses.parseNumericAddress(null);
+ fail(String.format("null is not numeric but was parsed as %s", inetAddress));
+ } catch (NullPointerException e) {
+ // expected
+ }
+ }
+
+ @Parameters(method = "validNumericAddressesAndStringRepresentation")
+ @Test
+ public void test_isNumericAddress(String address, String unused) {
+ assertTrue("expected '" + address + "' to be treated as numeric",
+ InetAddresses.isNumericAddress(address));
+ }
+
+ @Parameters(method = "invalidNumericAddresses")
+ @Test
+ public void test_isNotNumericAddress(String address) {
+ assertFalse("expected '" + address + "' to be treated as non-numeric",
+ InetAddresses.isNumericAddress(address));
+ }
+
+ @Test
+ public void test_isNumericAddress_null() {
+ try {
+ InetAddresses.isNumericAddress(null);
+ fail("expected null to throw a NullPointerException");
+ } catch (NullPointerException e) {
+ // expected
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
new file mode 100644
index 0000000..56ab2a7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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 android.net.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.IpConfiguration;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.net.InetAddressUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public final class IpConfigurationTest {
+ private static final LinkAddress LINKADDR = new LinkAddress("192.0.2.2/25");
+ private static final InetAddress GATEWAY = InetAddressUtils.parseNumericAddress("192.0.2.1");
+ private static final InetAddress DNS1 = InetAddressUtils.parseNumericAddress("8.8.8.8");
+ private static final InetAddress DNS2 = InetAddressUtils.parseNumericAddress("8.8.4.4");
+ private static final String DOMAINS = "example.com";
+
+ private static final ArrayList<InetAddress> dnsServers = new ArrayList<>();
+
+ private StaticIpConfiguration mStaticIpConfig;
+ private ProxyInfo mProxy;
+
+ @Before
+ public void setUp() {
+ dnsServers.add(DNS1);
+ dnsServers.add(DNS2);
+ mStaticIpConfig = new StaticIpConfiguration.Builder()
+ .setIpAddress(LINKADDR)
+ .setGateway(GATEWAY)
+ .setDnsServers(dnsServers)
+ .setDomains(DOMAINS)
+ .build();
+
+ mProxy = ProxyInfo.buildDirectProxy("test", 8888);
+ }
+
+ @Test
+ public void testConstructor() {
+ IpConfiguration ipConfig = new IpConfiguration();
+ checkEmpty(ipConfig);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration());
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setStaticIpConfiguration(mStaticIpConfig);
+ ipConfig.setHttpProxy(mProxy);
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.STATIC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.STATIC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+ }
+
+ private void checkEmpty(IpConfiguration config) {
+ assertEquals(IpConfiguration.IpAssignment.UNASSIGNED,
+ config.getIpAssignment().UNASSIGNED);
+ assertEquals(IpConfiguration.ProxySettings.UNASSIGNED,
+ config.getProxySettings().UNASSIGNED);
+ assertNull(config.getStaticIpConfiguration());
+ assertNull(config.getHttpProxy());
+ }
+
+ private void assertIpConfigurationEqual(IpConfiguration source, IpConfiguration target) {
+ assertEquals(source.getIpAssignment(), target.getIpAssignment());
+ assertEquals(source.getProxySettings(), target.getProxySettings());
+ assertEquals(source.getHttpProxy(), target.getHttpProxy());
+ assertEquals(source.getStaticIpConfiguration(), target.getStaticIpConfiguration());
+ }
+
+ @Test
+ public void testParcel() {
+ final IpConfiguration config = new IpConfiguration();
+ assertParcelSane(config, 4);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
new file mode 100644
index 0000000..10e43e7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.platform.test.annotations.AppModeFull;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+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.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class IpSecBaseTest {
+
+ private static final String TAG = IpSecBaseTest.class.getSimpleName();
+
+ protected static final String IPV4_LOOPBACK = "127.0.0.1";
+ protected static final String IPV6_LOOPBACK = "::1";
+ protected static final String[] LOOPBACK_ADDRS = new String[] {IPV4_LOOPBACK, IPV6_LOOPBACK};
+ protected static final int[] DIRECTIONS =
+ new int[] {IpSecManager.DIRECTION_IN, IpSecManager.DIRECTION_OUT};
+
+ protected static final byte[] TEST_DATA = "Best test data ever!".getBytes();
+ protected static final int DATA_BUFFER_LEN = 4096;
+ protected static final int SOCK_TIMEOUT = 500;
+
+ private static final byte[] KEY_DATA = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+ 0x20, 0x21, 0x22, 0x23
+ };
+
+ protected static final byte[] AUTH_KEY = getKey(256);
+ protected static final byte[] CRYPT_KEY = getKey(256);
+
+ protected ConnectivityManager mCM;
+ protected IpSecManager mISM;
+
+ @Before
+ public void setUp() throws Exception {
+ mISM =
+ (IpSecManager)
+ InstrumentationRegistry.getContext()
+ .getSystemService(Context.IPSEC_SERVICE);
+ mCM =
+ (ConnectivityManager)
+ InstrumentationRegistry.getContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ protected static byte[] getKey(int bitLength) {
+ return Arrays.copyOf(KEY_DATA, bitLength / 8);
+ }
+
+ protected static int getDomain(InetAddress address) {
+ int domain;
+ if (address instanceof Inet6Address) {
+ domain = OsConstants.AF_INET6;
+ } else {
+ domain = OsConstants.AF_INET;
+ }
+ return domain;
+ }
+
+ protected static int getPort(FileDescriptor sock) throws Exception {
+ return ((InetSocketAddress) Os.getsockname(sock)).getPort();
+ }
+
+ public static interface GenericSocket extends AutoCloseable {
+ void send(byte[] data) throws Exception;
+
+ byte[] receive() throws Exception;
+
+ int getPort() throws Exception;
+
+ void close() throws Exception;
+
+ void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception;
+
+ void removeTransportModeTransforms(IpSecManager ism) throws Exception;
+ }
+
+ public static interface GenericTcpSocket extends GenericSocket {}
+
+ public static interface GenericUdpSocket extends GenericSocket {
+ void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception;
+ }
+
+ public abstract static class NativeSocket implements GenericSocket {
+ public FileDescriptor mFd;
+
+ public NativeSocket(FileDescriptor fd) {
+ mFd = fd;
+ }
+
+ @Override
+ public void send(byte[] data) throws Exception {
+ Os.write(mFd, data, 0, data.length);
+ }
+
+ @Override
+ public byte[] receive() throws Exception {
+ byte[] in = new byte[DATA_BUFFER_LEN];
+ AtomicInteger bytesRead = new AtomicInteger(-1);
+
+ Thread readSockThread = new Thread(() -> {
+ long startTime = System.currentTimeMillis();
+ while (bytesRead.get() < 0 && System.currentTimeMillis() < startTime + SOCK_TIMEOUT) {
+ try {
+ bytesRead.set(Os.recvfrom(mFd, in, 0, DATA_BUFFER_LEN, 0, null));
+ } catch (Exception e) {
+ Log.e(TAG, "Error encountered reading from socket", e);
+ }
+ }
+ });
+
+ readSockThread.start();
+ readSockThread.join(SOCK_TIMEOUT);
+
+ if (bytesRead.get() < 0) {
+ throw new IOException("No data received from socket");
+ }
+
+ return Arrays.copyOfRange(in, 0, bytesRead.get());
+ }
+
+ @Override
+ public int getPort() throws Exception {
+ return IpSecBaseTest.getPort(mFd);
+ }
+
+ @Override
+ public void close() throws Exception {
+ Os.close(mFd);
+ }
+
+ @Override
+ public void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception {
+ ism.applyTransportModeTransform(mFd, direction, transform);
+ }
+
+ @Override
+ public void removeTransportModeTransforms(IpSecManager ism) throws Exception {
+ ism.removeTransportModeTransforms(mFd);
+ }
+ }
+
+ public static class NativeTcpSocket extends NativeSocket implements GenericTcpSocket {
+ public NativeTcpSocket(FileDescriptor fd) {
+ super(fd);
+ }
+ }
+
+ public static class NativeUdpSocket extends NativeSocket implements GenericUdpSocket {
+ public NativeUdpSocket(FileDescriptor fd) {
+ super(fd);
+ }
+
+ @Override
+ public void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception {
+ Os.sendto(mFd, data, 0, data.length, 0, dstAddr, port);
+ }
+ }
+
+ public static class JavaUdpSocket implements GenericUdpSocket {
+ public final DatagramSocket mSocket;
+
+ public JavaUdpSocket(InetAddress localAddr, int port) {
+ try {
+ mSocket = new DatagramSocket(port, localAddr);
+ mSocket.setSoTimeout(SOCK_TIMEOUT);
+ } catch (SocketException e) {
+ // Fail loudly if we can't set up sockets properly. And without the timeout, we
+ // could easily end up in an endless wait.
+ throw new RuntimeException(e);
+ }
+ }
+
+ public JavaUdpSocket(InetAddress localAddr) {
+ try {
+ mSocket = new DatagramSocket(0, localAddr);
+ mSocket.setSoTimeout(SOCK_TIMEOUT);
+ } catch (SocketException e) {
+ // Fail loudly if we can't set up sockets properly. And without the timeout, we
+ // could easily end up in an endless wait.
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void send(byte[] data) throws Exception {
+ mSocket.send(new DatagramPacket(data, data.length));
+ }
+
+ @Override
+ public void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception {
+ mSocket.send(new DatagramPacket(data, data.length, dstAddr, port));
+ }
+
+ @Override
+ public int getPort() throws Exception {
+ return mSocket.getLocalPort();
+ }
+
+ @Override
+ public void close() throws Exception {
+ mSocket.close();
+ }
+
+ @Override
+ public byte[] receive() throws Exception {
+ DatagramPacket data = new DatagramPacket(new byte[DATA_BUFFER_LEN], DATA_BUFFER_LEN);
+ mSocket.receive(data);
+ return Arrays.copyOfRange(data.getData(), 0, data.getLength());
+ }
+
+ @Override
+ public void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception {
+ ism.applyTransportModeTransform(mSocket, direction, transform);
+ }
+
+ @Override
+ public void removeTransportModeTransforms(IpSecManager ism) throws Exception {
+ ism.removeTransportModeTransforms(mSocket);
+ }
+ }
+
+ public static class JavaTcpSocket implements GenericTcpSocket {
+ public final Socket mSocket;
+
+ public JavaTcpSocket(Socket socket) {
+ mSocket = socket;
+ try {
+ mSocket.setSoTimeout(SOCK_TIMEOUT);
+ } catch (SocketException e) {
+ // Fail loudly if we can't set up sockets properly. And without the timeout, we
+ // could easily end up in an endless wait.
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void send(byte[] data) throws Exception {
+ mSocket.getOutputStream().write(data);
+ }
+
+ @Override
+ public byte[] receive() throws Exception {
+ byte[] in = new byte[DATA_BUFFER_LEN];
+ int bytesRead = mSocket.getInputStream().read(in);
+ return Arrays.copyOfRange(in, 0, bytesRead);
+ }
+
+ @Override
+ public int getPort() throws Exception {
+ return mSocket.getLocalPort();
+ }
+
+ @Override
+ public void close() throws Exception {
+ mSocket.close();
+ }
+
+ @Override
+ public void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception {
+ ism.applyTransportModeTransform(mSocket, direction, transform);
+ }
+
+ @Override
+ public void removeTransportModeTransforms(IpSecManager ism) throws Exception {
+ ism.removeTransportModeTransforms(mSocket);
+ }
+ }
+
+ public static class SocketPair<T> {
+ public final T mLeftSock;
+ public final T mRightSock;
+
+ public SocketPair(T leftSock, T rightSock) {
+ mLeftSock = leftSock;
+ mRightSock = rightSock;
+ }
+ }
+
+ protected static void applyTransformBidirectionally(
+ IpSecManager ism, IpSecTransform transform, GenericSocket socket) throws Exception {
+ for (int direction : DIRECTIONS) {
+ socket.applyTransportModeTransform(ism, direction, transform);
+ }
+ }
+
+ public static SocketPair<NativeUdpSocket> getNativeUdpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform, boolean connected)
+ throws Exception {
+ int domain = getDomain(localAddr);
+
+ NativeUdpSocket leftSock = new NativeUdpSocket(
+ Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP));
+ NativeUdpSocket rightSock = new NativeUdpSocket(
+ Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP));
+
+ for (NativeUdpSocket sock : new NativeUdpSocket[] {leftSock, rightSock}) {
+ applyTransformBidirectionally(ism, transform, sock);
+ Os.bind(sock.mFd, localAddr, 0);
+ }
+
+ if (connected) {
+ Os.connect(leftSock.mFd, localAddr, rightSock.getPort());
+ Os.connect(rightSock.mFd, localAddr, leftSock.getPort());
+ }
+
+ return new SocketPair<>(leftSock, rightSock);
+ }
+
+ public static SocketPair<NativeTcpSocket> getNativeTcpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception {
+ int domain = getDomain(localAddr);
+
+ NativeTcpSocket server = new NativeTcpSocket(
+ Os.socket(domain, OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP));
+ NativeTcpSocket client = new NativeTcpSocket(
+ Os.socket(domain, OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP));
+
+ Os.bind(server.mFd, localAddr, 0);
+
+ applyTransformBidirectionally(ism, transform, server);
+ applyTransformBidirectionally(ism, transform, client);
+
+ Os.listen(server.mFd, 10);
+ Os.connect(client.mFd, localAddr, server.getPort());
+ NativeTcpSocket accepted = new NativeTcpSocket(Os.accept(server.mFd, null));
+
+ applyTransformBidirectionally(ism, transform, accepted);
+ server.close();
+
+ return new SocketPair<>(client, accepted);
+ }
+
+ public static SocketPair<JavaUdpSocket> getJavaUdpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform, boolean connected)
+ throws Exception {
+ JavaUdpSocket leftSock = new JavaUdpSocket(localAddr);
+ JavaUdpSocket rightSock = new JavaUdpSocket(localAddr);
+
+ applyTransformBidirectionally(ism, transform, leftSock);
+ applyTransformBidirectionally(ism, transform, rightSock);
+
+ if (connected) {
+ leftSock.mSocket.connect(localAddr, rightSock.mSocket.getLocalPort());
+ rightSock.mSocket.connect(localAddr, leftSock.mSocket.getLocalPort());
+ }
+
+ return new SocketPair<>(leftSock, rightSock);
+ }
+
+ public static SocketPair<JavaTcpSocket> getJavaTcpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception {
+ JavaTcpSocket clientSock = new JavaTcpSocket(new Socket());
+ ServerSocket serverSocket = new ServerSocket();
+ serverSocket.bind(new InetSocketAddress(localAddr, 0));
+
+ // While technically the client socket does not need to be bound, the OpenJDK implementation
+ // of Socket only allocates an FD when bind() or connect() or other similar methods are
+ // called. So we call bind to force the FD creation, so that we can apply a transform to it
+ // prior to socket connect.
+ clientSock.mSocket.bind(new InetSocketAddress(localAddr, 0));
+
+ // IpSecService doesn't support serverSockets at the moment; workaround using FD
+ FileDescriptor serverFd = serverSocket.getImpl().getFD$();
+
+ applyTransformBidirectionally(ism, transform, new NativeTcpSocket(serverFd));
+ applyTransformBidirectionally(ism, transform, clientSock);
+
+ clientSock.mSocket.connect(new InetSocketAddress(localAddr, serverSocket.getLocalPort()));
+ JavaTcpSocket acceptedSock = new JavaTcpSocket(serverSocket.accept());
+
+ applyTransformBidirectionally(ism, transform, acceptedSock);
+ serverSocket.close();
+
+ return new SocketPair<>(clientSock, acceptedSock);
+ }
+
+ private void checkSocketPair(GenericSocket left, GenericSocket right) throws Exception {
+ left.send(TEST_DATA);
+ assertArrayEquals(TEST_DATA, right.receive());
+
+ right.send(TEST_DATA);
+ assertArrayEquals(TEST_DATA, left.receive());
+
+ left.close();
+ right.close();
+ }
+
+ private void checkUnconnectedUdpSocketPair(
+ GenericUdpSocket left, GenericUdpSocket right, InetAddress localAddr) throws Exception {
+ left.sendTo(TEST_DATA, localAddr, right.getPort());
+ assertArrayEquals(TEST_DATA, right.receive());
+
+ right.sendTo(TEST_DATA, localAddr, left.getPort());
+ assertArrayEquals(TEST_DATA, left.receive());
+
+ left.close();
+ right.close();
+ }
+
+ protected static IpSecTransform buildIpSecTransform(
+ Context context,
+ IpSecManager.SecurityParameterIndex spi,
+ IpSecManager.UdpEncapsulationSocket encapSocket,
+ InetAddress remoteAddr)
+ throws Exception {
+ IpSecTransform.Builder builder =
+ new IpSecTransform.Builder(context)
+ .setEncryption(new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY))
+ .setAuthentication(
+ new IpSecAlgorithm(
+ IpSecAlgorithm.AUTH_HMAC_SHA256,
+ AUTH_KEY,
+ AUTH_KEY.length * 4));
+
+ if (encapSocket != null) {
+ builder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+ }
+
+ return builder.buildTransportModeTransform(remoteAddr, spi);
+ }
+
+ private IpSecTransform buildDefaultTransform(InetAddress localAddr) throws Exception {
+ try (IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(localAddr)) {
+ return buildIpSecTransform(InstrumentationRegistry.getContext(), spi, null, localAddr);
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testJavaTcpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<JavaTcpSocket> sockets = getJavaTcpSocketPair(local, mISM, transform);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testJavaUdpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<JavaUdpSocket> sockets =
+ getJavaUdpSocketPair(local, mISM, transform, true);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testJavaUdpSocketPairUnconnected() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<JavaUdpSocket> sockets =
+ getJavaUdpSocketPair(local, mISM, transform, false);
+ checkUnconnectedUdpSocketPair(sockets.mLeftSock, sockets.mRightSock, local);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testNativeTcpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<NativeTcpSocket> sockets =
+ getNativeTcpSocketPair(local, mISM, transform);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testNativeUdpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<NativeUdpSocket> sockets =
+ getNativeUdpSocketPair(local, mISM, transform, true);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testNativeUdpSocketPairUnconnected() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<NativeUdpSocket> sockets =
+ getNativeUdpSocketPair(local, mISM, transform, false);
+ checkUnconnectedUdpSocketPair(sockets.mLeftSock, sockets.mRightSock, local);
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
new file mode 100644
index 0000000..d08f6e9
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -0,0 +1,1200 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
+import static android.net.cts.PacketUtils.AES_GCM_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_GCM_IV_LEN;
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.TCP_HDRLEN_WITH_TIMESTAMP_OPT;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.TrafficStats;
+import android.platform.test.annotations.AppModeFull;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.SkipPresubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+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.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+public class IpSecManagerTest extends IpSecBaseTest {
+
+ private static final String TAG = IpSecManagerTest.class.getSimpleName();
+
+ private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");
+ private static final InetAddress GOOGLE_DNS_6 =
+ InetAddress.parseNumericAddress("2001:4860:4860::8888");
+
+ private static final InetAddress[] GOOGLE_DNS_LIST =
+ new InetAddress[] {GOOGLE_DNS_4, GOOGLE_DNS_6};
+
+ private static final int DROID_SPI = 0xD1201D;
+ private static final int MAX_PORT_BIND_ATTEMPTS = 10;
+
+ private static final byte[] AEAD_KEY = getKey(288);
+
+ /*
+ * Allocate a random SPI
+ * Allocate a specific SPI using previous randomly created SPI value
+ * Realloc the same SPI that was specifically created (expect SpiUnavailable)
+ * Close SPIs
+ */
+ @Test
+ public void testAllocSpi() throws Exception {
+ for (InetAddress addr : GOOGLE_DNS_LIST) {
+ IpSecManager.SecurityParameterIndex randomSpi = null, droidSpi = null;
+ randomSpi = mISM.allocateSecurityParameterIndex(addr);
+ assertTrue(
+ "Failed to receive a valid SPI",
+ randomSpi.getSpi() != IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
+
+ droidSpi = mISM.allocateSecurityParameterIndex(addr, DROID_SPI);
+ assertTrue("Failed to allocate specified SPI, " + DROID_SPI,
+ droidSpi.getSpi() == DROID_SPI);
+
+ try {
+ mISM.allocateSecurityParameterIndex(addr, DROID_SPI);
+ fail("Duplicate SPI was allowed to be created");
+ } catch (IpSecManager.SpiUnavailableException expected) {
+ // This is a success case because we expect a dupe SPI to throw
+ }
+
+ randomSpi.close();
+ droidSpi.close();
+ }
+ }
+
+ /** This function finds an available port */
+ private static int findUnusedPort() throws Exception {
+ // Get an available port.
+ DatagramSocket s = new DatagramSocket();
+ int port = s.getLocalPort();
+ s.close();
+ return port;
+ }
+
+ private static FileDescriptor getBoundUdpSocket(InetAddress address) throws Exception {
+ FileDescriptor sock =
+ Os.socket(getDomain(address), OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP);
+
+ for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) {
+ try {
+ int port = findUnusedPort();
+ Os.bind(sock, address, port);
+ break;
+ } catch (ErrnoException e) {
+ // Someone claimed the port since we called findUnusedPort.
+ if (e.errno == OsConstants.EADDRINUSE) {
+ if (i == MAX_PORT_BIND_ATTEMPTS - 1) {
+
+ fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port");
+ }
+ continue;
+ }
+ throw e.rethrowAsIOException();
+ }
+ }
+ return sock;
+ }
+
+ private void checkUnconnectedUdp(IpSecTransform transform, InetAddress local, int sendCount,
+ boolean useJavaSockets) throws Exception {
+ GenericUdpSocket sockLeft = null, sockRight = null;
+ if (useJavaSockets) {
+ SocketPair<JavaUdpSocket> sockets = getJavaUdpSocketPair(local, mISM, transform, false);
+ sockLeft = sockets.mLeftSock;
+ sockRight = sockets.mRightSock;
+ } else {
+ SocketPair<NativeUdpSocket> sockets =
+ getNativeUdpSocketPair(local, mISM, transform, false);
+ sockLeft = sockets.mLeftSock;
+ sockRight = sockets.mRightSock;
+ }
+
+ for (int i = 0; i < sendCount; i++) {
+ byte[] in;
+
+ sockLeft.sendTo(TEST_DATA, local, sockRight.getPort());
+ in = sockRight.receive();
+ assertArrayEquals("Left-to-right encrypted data did not match.", TEST_DATA, in);
+
+ sockRight.sendTo(TEST_DATA, local, sockLeft.getPort());
+ in = sockLeft.receive();
+ assertArrayEquals("Right-to-left encrypted data did not match.", TEST_DATA, in);
+ }
+
+ sockLeft.close();
+ sockRight.close();
+ }
+
+ private void checkTcp(IpSecTransform transform, InetAddress local, int sendCount,
+ boolean useJavaSockets) throws Exception {
+ GenericTcpSocket client = null, accepted = null;
+ if (useJavaSockets) {
+ SocketPair<JavaTcpSocket> sockets = getJavaTcpSocketPair(local, mISM, transform);
+ client = sockets.mLeftSock;
+ accepted = sockets.mRightSock;
+ } else {
+ SocketPair<NativeTcpSocket> sockets = getNativeTcpSocketPair(local, mISM, transform);
+ client = sockets.mLeftSock;
+ accepted = sockets.mRightSock;
+ }
+
+ // Wait for TCP handshake packets to be counted
+ StatsChecker.waitForNumPackets(3); // (SYN, SYN+ACK, ACK)
+
+ // Reset StatsChecker, to ignore negotiation overhead.
+ StatsChecker.initStatsChecker();
+ for (int i = 0; i < sendCount; i++) {
+ byte[] in;
+
+ client.send(TEST_DATA);
+ in = accepted.receive();
+ assertArrayEquals("Client-to-server encrypted data did not match.", TEST_DATA, in);
+
+ // Allow for newest data + ack packets to be returned before sending next packet
+ // Also add the number of expected packets in each of the previous runs (4 per run)
+ StatsChecker.waitForNumPackets(2 + (4 * i));
+
+ accepted.send(TEST_DATA);
+ in = client.receive();
+ assertArrayEquals("Server-to-client encrypted data did not match.", TEST_DATA, in);
+
+ // Allow for all data + ack packets to be returned before sending next packet
+ // Also add the number of expected packets in each of the previous runs (4 per run)
+ StatsChecker.waitForNumPackets(4 * (i + 1));
+ }
+
+ // Transforms should not be removed from the sockets, otherwise FIN packets will be sent
+ // unencrypted.
+ // This test also unfortunately happens to rely on a nuance of the cleanup order. By
+ // keeping the policy on the socket, but removing the SA before lingering FIN packets
+ // are sent (at an undetermined later time), the FIN packets are dropped. Without this,
+ // we run into all kinds of headaches trying to test data accounting (unsolicited
+ // packets mysteriously appearing and messing up our counters)
+ // The right way to close sockets is to set SO_LINGER to ensure synchronous closure,
+ // closing the sockets, and then closing the transforms. See documentation for the
+ // Socket or FileDescriptor flavors of applyTransportModeTransform() in IpSecManager
+ // for more details.
+
+ client.close();
+ accepted.close();
+ }
+
+ /*
+ * Alloc outbound SPI
+ * Alloc inbound SPI
+ * Create transport mode transform
+ * open socket
+ * apply transform to socket
+ * send data on socket
+ * release transform
+ * send data (expect exception)
+ */
+ @Test
+ public void testCreateTransform() throws Exception {
+ InetAddress localAddr = InetAddress.getByName(IPV4_LOOPBACK);
+ IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(localAddr);
+
+ IpSecTransform transform =
+ new IpSecTransform.Builder(InstrumentationRegistry.getContext())
+ .setEncryption(new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY))
+ .setAuthentication(
+ new IpSecAlgorithm(
+ IpSecAlgorithm.AUTH_HMAC_SHA256,
+ AUTH_KEY,
+ AUTH_KEY.length * 8))
+ .buildTransportModeTransform(localAddr, spi);
+
+ final boolean [][] applyInApplyOut = {
+ {false, false}, {false, true}, {true, false}, {true,true}};
+ final byte[] data = new String("Best test data ever!").getBytes("UTF-8");
+ final DatagramPacket outPacket = new DatagramPacket(data, 0, data.length, localAddr, 0);
+
+ byte[] in = new byte[data.length];
+ DatagramPacket inPacket = new DatagramPacket(in, in.length);
+ DatagramSocket localSocket;
+ int localPort;
+
+ for(boolean[] io : applyInApplyOut) {
+ boolean applyIn = io[0];
+ boolean applyOut = io[1];
+ // Bind localSocket to a random available port.
+ localSocket = new DatagramSocket(0);
+ localPort = localSocket.getLocalPort();
+ localSocket.setSoTimeout(200);
+ outPacket.setPort(localPort);
+ if (applyIn) {
+ mISM.applyTransportModeTransform(
+ localSocket, IpSecManager.DIRECTION_IN, transform);
+ }
+ if (applyOut) {
+ mISM.applyTransportModeTransform(
+ localSocket, IpSecManager.DIRECTION_OUT, transform);
+ }
+ if (applyIn == applyOut) {
+ localSocket.send(outPacket);
+ localSocket.receive(inPacket);
+ assertTrue("Encapsulated data did not match.",
+ Arrays.equals(outPacket.getData(), inPacket.getData()));
+ mISM.removeTransportModeTransforms(localSocket);
+ localSocket.close();
+ } else {
+ try {
+ localSocket.send(outPacket);
+ localSocket.receive(inPacket);
+ } catch (IOException e) {
+ continue;
+ } finally {
+ mISM.removeTransportModeTransforms(localSocket);
+ localSocket.close();
+ }
+ // FIXME: This check is disabled because sockets currently receive data
+ // if there is a valid SA for decryption, even when the input policy is
+ // not applied to a socket.
+ // fail("Data IO should fail on asymmetrical transforms! + Input="
+ // + applyIn + " Output=" + applyOut);
+ }
+ }
+ transform.close();
+ }
+
+ /** Snapshot of TrafficStats as of initStatsChecker call for later comparisons */
+ private static class StatsChecker {
+ private static final double ERROR_MARGIN_BYTES = 1.05;
+ private static final double ERROR_MARGIN_PKTS = 1.05;
+ private static final int MAX_WAIT_TIME_MILLIS = 1000;
+
+ private static long uidTxBytes;
+ private static long uidRxBytes;
+ private static long uidTxPackets;
+ private static long uidRxPackets;
+
+ private static long ifaceTxBytes;
+ private static long ifaceRxBytes;
+ private static long ifaceTxPackets;
+ private static long ifaceRxPackets;
+
+ /**
+ * This method counts the number of incoming packets, polling intermittently up to
+ * MAX_WAIT_TIME_MILLIS.
+ */
+ private static void waitForNumPackets(int numPackets) throws Exception {
+ long uidTxDelta = 0;
+ long uidRxDelta = 0;
+ for (int i = 0; i < 100; i++) {
+ uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
+ uidRxDelta = TrafficStats.getUidRxPackets(Os.getuid()) - uidRxPackets;
+
+ // TODO: Check Rx packets as well once kernel security policy bug is fixed.
+ // (b/70635417)
+ if (uidTxDelta >= numPackets) {
+ return;
+ }
+ Thread.sleep(MAX_WAIT_TIME_MILLIS / 100);
+ }
+ fail(
+ "Not enough traffic was recorded to satisfy the provided conditions: wanted "
+ + numPackets
+ + ", got "
+ + uidTxDelta
+ + " tx and "
+ + uidRxDelta
+ + " rx packets");
+ }
+
+ private static void assertUidStatsDelta(
+ int expectedTxByteDelta,
+ int expectedTxPacketDelta,
+ int minRxByteDelta,
+ int maxRxByteDelta,
+ int expectedRxPacketDelta) {
+ long newUidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
+ long newUidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
+ long newUidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
+ long newUidRxPackets = TrafficStats.getUidRxPackets(Os.getuid());
+
+ assertEquals(expectedTxByteDelta, newUidTxBytes - uidTxBytes);
+ assertTrue(
+ newUidRxBytes - uidRxBytes >= minRxByteDelta
+ && newUidRxBytes - uidRxBytes <= maxRxByteDelta);
+ assertEquals(expectedTxPacketDelta, newUidTxPackets - uidTxPackets);
+ assertEquals(expectedRxPacketDelta, newUidRxPackets - uidRxPackets);
+ }
+
+ private static void assertIfaceStatsDelta(
+ int expectedTxByteDelta,
+ int expectedTxPacketDelta,
+ int expectedRxByteDelta,
+ int expectedRxPacketDelta)
+ throws IOException {
+ long newIfaceTxBytes = TrafficStats.getLoopbackTxBytes();
+ long newIfaceRxBytes = TrafficStats.getLoopbackRxBytes();
+ long newIfaceTxPackets = TrafficStats.getLoopbackTxPackets();
+ long newIfaceRxPackets = TrafficStats.getLoopbackRxPackets();
+
+ // Check that iface stats are within an acceptable range; data might be sent
+ // on the local interface by other apps.
+ assertApproxEquals(
+ ifaceTxBytes, newIfaceTxBytes, expectedTxByteDelta, ERROR_MARGIN_BYTES);
+ assertApproxEquals(
+ ifaceRxBytes, newIfaceRxBytes, expectedRxByteDelta, ERROR_MARGIN_BYTES);
+ assertApproxEquals(
+ ifaceTxPackets, newIfaceTxPackets, expectedTxPacketDelta, ERROR_MARGIN_PKTS);
+ assertApproxEquals(
+ ifaceRxPackets, newIfaceRxPackets, expectedRxPacketDelta, ERROR_MARGIN_PKTS);
+ }
+
+ private static void assertApproxEquals(
+ long oldStats, long newStats, int expectedDelta, double errorMargin) {
+ assertTrue(expectedDelta <= newStats - oldStats);
+ assertTrue((expectedDelta * errorMargin) > newStats - oldStats);
+ }
+
+ private static void initStatsChecker() throws Exception {
+ uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
+ uidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
+ uidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
+ uidRxPackets = TrafficStats.getUidRxPackets(Os.getuid());
+
+ ifaceTxBytes = TrafficStats.getLoopbackTxBytes();
+ ifaceRxBytes = TrafficStats.getLoopbackRxBytes();
+ ifaceTxPackets = TrafficStats.getLoopbackTxPackets();
+ ifaceRxPackets = TrafficStats.getLoopbackRxPackets();
+ }
+ }
+
+ private int getTruncLenBits(IpSecAlgorithm authOrAead) {
+ return authOrAead == null ? 0 : authOrAead.getTruncationLengthBits();
+ }
+
+ private int getIvLen(IpSecAlgorithm cryptOrAead) {
+ if (cryptOrAead == null) { return 0; }
+
+ switch (cryptOrAead.getName()) {
+ case IpSecAlgorithm.CRYPT_AES_CBC:
+ return AES_CBC_IV_LEN;
+ case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
+ return AES_GCM_IV_LEN;
+ default:
+ throw new IllegalArgumentException(
+ "IV length unknown for algorithm" + cryptOrAead.getName());
+ }
+ }
+
+ private int getBlkSize(IpSecAlgorithm cryptOrAead) {
+ // RFC 4303, section 2.4 states that ciphertext plus pad_len, next_header fields must
+ // terminate on a 4-byte boundary. Thus, the minimum ciphertext block size is 4 bytes.
+ if (cryptOrAead == null) { return 4; }
+
+ switch (cryptOrAead.getName()) {
+ case IpSecAlgorithm.CRYPT_AES_CBC:
+ return AES_CBC_BLK_SIZE;
+ case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
+ return AES_GCM_BLK_SIZE;
+ default:
+ throw new IllegalArgumentException(
+ "Blk size unknown for algorithm" + cryptOrAead.getName());
+ }
+ }
+
+ public void checkTransform(
+ int protocol,
+ String localAddress,
+ IpSecAlgorithm crypt,
+ IpSecAlgorithm auth,
+ IpSecAlgorithm aead,
+ boolean doUdpEncap,
+ int sendCount,
+ boolean useJavaSockets)
+ throws Exception {
+ StatsChecker.initStatsChecker();
+ InetAddress local = InetAddress.getByName(localAddress);
+
+ try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket();
+ IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(local)) {
+
+ IpSecTransform.Builder transformBuilder =
+ new IpSecTransform.Builder(InstrumentationRegistry.getContext());
+ if (crypt != null) {
+ transformBuilder.setEncryption(crypt);
+ }
+ if (auth != null) {
+ transformBuilder.setAuthentication(auth);
+ }
+ if (aead != null) {
+ transformBuilder.setAuthenticatedEncryption(aead);
+ }
+
+ if (doUdpEncap) {
+ transformBuilder =
+ transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+ }
+
+ int ipHdrLen = local instanceof Inet6Address ? IP6_HDRLEN : IP4_HDRLEN;
+ int transportHdrLen = 0;
+ int udpEncapLen = doUdpEncap ? UDP_HDRLEN : 0;
+
+ try (IpSecTransform transform =
+ transformBuilder.buildTransportModeTransform(local, spi)) {
+ if (protocol == IPPROTO_TCP) {
+ transportHdrLen = TCP_HDRLEN_WITH_TIMESTAMP_OPT;
+ checkTcp(transform, local, sendCount, useJavaSockets);
+ } else if (protocol == IPPROTO_UDP) {
+ transportHdrLen = UDP_HDRLEN;
+
+ // TODO: Also check connected udp.
+ checkUnconnectedUdp(transform, local, sendCount, useJavaSockets);
+ } else {
+ throw new IllegalArgumentException("Invalid protocol");
+ }
+ }
+
+ checkStatsChecker(
+ protocol,
+ ipHdrLen,
+ transportHdrLen,
+ udpEncapLen,
+ sendCount,
+ getIvLen(crypt != null ? crypt : aead),
+ getBlkSize(crypt != null ? crypt : aead),
+ getTruncLenBits(auth != null ? auth : aead));
+ }
+ }
+
+ private void checkStatsChecker(
+ int protocol,
+ int ipHdrLen,
+ int transportHdrLen,
+ int udpEncapLen,
+ int sendCount,
+ int ivLen,
+ int blkSize,
+ int truncLenBits)
+ throws Exception {
+
+ int innerPacketSize = TEST_DATA.length + transportHdrLen + ipHdrLen;
+ int outerPacketSize =
+ PacketUtils.calculateEspPacketSize(
+ TEST_DATA.length + transportHdrLen, ivLen, blkSize, truncLenBits)
+ + udpEncapLen
+ + ipHdrLen;
+
+ int expectedOuterBytes = outerPacketSize * sendCount;
+ int expectedInnerBytes = innerPacketSize * sendCount;
+ int expectedPackets = sendCount;
+
+ // Each run sends two packets, one in each direction.
+ sendCount *= 2;
+ expectedOuterBytes *= 2;
+ expectedInnerBytes *= 2;
+ expectedPackets *= 2;
+
+ // Add TCP ACKs for data packets
+ if (protocol == IPPROTO_TCP) {
+ int encryptedTcpPktSize =
+ PacketUtils.calculateEspPacketSize(
+ TCP_HDRLEN_WITH_TIMESTAMP_OPT, ivLen, blkSize, truncLenBits);
+
+ // Add data packet ACKs
+ expectedOuterBytes += (encryptedTcpPktSize + udpEncapLen + ipHdrLen) * (sendCount);
+ expectedInnerBytes += (TCP_HDRLEN_WITH_TIMESTAMP_OPT + ipHdrLen) * (sendCount);
+ expectedPackets += sendCount;
+ }
+
+ StatsChecker.waitForNumPackets(expectedPackets);
+
+ // eBPF only counts inner packets, whereas xt_qtaguid counts outer packets. Allow both
+ StatsChecker.assertUidStatsDelta(
+ expectedOuterBytes,
+ expectedPackets,
+ expectedInnerBytes,
+ expectedOuterBytes,
+ expectedPackets);
+
+ // Unreliable at low numbers due to potential interference from other processes.
+ if (sendCount >= 1000) {
+ StatsChecker.assertIfaceStatsDelta(
+ expectedOuterBytes, expectedPackets, expectedOuterBytes, expectedPackets);
+ }
+ }
+
+ private void checkIkePacket(
+ NativeUdpSocket wrappedEncapSocket, InetAddress localAddr) throws Exception {
+ StatsChecker.initStatsChecker();
+
+ try (NativeUdpSocket remoteSocket = new NativeUdpSocket(getBoundUdpSocket(localAddr))) {
+
+ // Append IKE/ESP header - 4 bytes of SPI, 4 bytes of seq number, all zeroed out
+ // If the first four bytes are zero, assume non-ESP (IKE traffic)
+ byte[] dataWithEspHeader = new byte[TEST_DATA.length + 8];
+ System.arraycopy(TEST_DATA, 0, dataWithEspHeader, 8, TEST_DATA.length);
+
+ // Send the IKE packet from remoteSocket to wrappedEncapSocket. Since IKE packets
+ // are multiplexed over the socket, we expect them to appear on the encap socket
+ // (as opposed to being decrypted and received on the non-encap socket)
+ remoteSocket.sendTo(dataWithEspHeader, localAddr, wrappedEncapSocket.getPort());
+ byte[] in = wrappedEncapSocket.receive();
+ assertArrayEquals("Encapsulated data did not match.", dataWithEspHeader, in);
+
+ // Also test that the IKE socket can send data out.
+ wrappedEncapSocket.sendTo(dataWithEspHeader, localAddr, remoteSocket.getPort());
+ in = remoteSocket.receive();
+ assertArrayEquals("Encapsulated data did not match.", dataWithEspHeader, in);
+
+ // Calculate expected packet sizes. Always use IPv4 header, since our kernels only
+ // guarantee support of UDP encap on IPv4.
+ int expectedNumPkts = 2;
+ int expectedPacketSize =
+ expectedNumPkts * (dataWithEspHeader.length + UDP_HDRLEN + IP4_HDRLEN);
+
+ StatsChecker.waitForNumPackets(expectedNumPkts);
+ StatsChecker.assertUidStatsDelta(
+ expectedPacketSize,
+ expectedNumPkts,
+ expectedPacketSize,
+ expectedPacketSize,
+ expectedNumPkts);
+ StatsChecker.assertIfaceStatsDelta(
+ expectedPacketSize, expectedNumPkts, expectedPacketSize, expectedNumPkts);
+ }
+ }
+
+ @Test
+ public void testIkeOverUdpEncapSocket() throws Exception {
+ // IPv6 not supported for UDP-encap-ESP
+ InetAddress local = InetAddress.getByName(IPV4_LOOPBACK);
+ try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+ NativeUdpSocket wrappedEncapSocket =
+ new NativeUdpSocket(encapSocket.getFileDescriptor());
+ checkIkePacket(wrappedEncapSocket, local);
+
+ // Now try with a transform applied to a socket using this Encap socket
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+
+ try (IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(local);
+ IpSecTransform transform =
+ new IpSecTransform.Builder(InstrumentationRegistry.getContext())
+ .setEncryption(crypt)
+ .setAuthentication(auth)
+ .setIpv4Encapsulation(encapSocket, encapSocket.getPort())
+ .buildTransportModeTransform(local, spi);
+ JavaUdpSocket localSocket = new JavaUdpSocket(local)) {
+ applyTransformBidirectionally(mISM, transform, localSocket);
+
+ checkIkePacket(wrappedEncapSocket, local);
+ }
+ }
+ }
+
+ // TODO: Check IKE over ESP sockets (IPv4, IPv6) - does this need SOCK_RAW?
+
+ /* TODO: Re-enable these when policy matcher works for reflected packets
+ *
+ * The issue here is that A sends to B, and everything is new; therefore PREROUTING counts
+ * correctly. But it appears that the security path is not cleared afterwards, thus when A
+ * sends an ACK back to B, the policy matcher flags it as a "IPSec" packet. See b/70635417
+ */
+
+ // public void testInterfaceCountersTcp4() throws Exception {
+ // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ // IpSecAlgorithm auth = new IpSecAlgorithm(
+ // IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ // checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, false, 1000);
+ // }
+
+ // public void testInterfaceCountersTcp6() throws Exception {
+ // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ // IpSecAlgorithm auth = new IpSecAlgorithm(
+ // IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ // checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, false, 1000);
+ // }
+
+ // public void testInterfaceCountersTcp4UdpEncap() throws Exception {
+ // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ // IpSecAlgorithm auth =
+ // new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ // checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, true, 1000);
+ // }
+
+ @Test
+ public void testInterfaceCountersUdp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1000, false);
+ }
+
+ @Test
+ public void testInterfaceCountersUdp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1000, false);
+ }
+
+ @Test
+ public void testInterfaceCountersUdp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1000, false);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesCbcHmacMd5Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesCbcHmacSha1Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesCbcHmacSha256Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesCbcHmacSha384Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesCbcHmacSha512Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Tcp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesGcm64Tcp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Udp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Udp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Tcp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesGcm96Tcp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Udp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Udp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Tcp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAesGcm128Tcp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Udp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Udp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testCryptUdp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthUdp4() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptUdp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthUdp6() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptTcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthTcp4() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testCryptTcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+ public void testAuthTcp6() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptUdp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, true, 1, true);
+ }
+
+ @Test
+ public void testAuthUdp4UdpEncap() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testCryptTcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, true, 1, true);
+ }
+
+ @Test
+ public void testAuthTcp4UdpEncap() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testOpenUdpEncapSocketSpecificPort() throws Exception {
+ IpSecManager.UdpEncapsulationSocket encapSocket = null;
+ int port = -1;
+ for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) {
+ try {
+ port = findUnusedPort();
+ encapSocket = mISM.openUdpEncapsulationSocket(port);
+ break;
+ } catch (ErrnoException e) {
+ if (e.errno == OsConstants.EADDRINUSE) {
+ // Someone claimed the port since we called findUnusedPort.
+ continue;
+ }
+ throw e;
+ } finally {
+ if (encapSocket != null) {
+ encapSocket.close();
+ }
+ }
+ }
+
+ if (encapSocket == null) {
+ fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port");
+ }
+
+ assertTrue("Returned invalid port", encapSocket.getPort() == port);
+ }
+
+ @Test
+ public void testOpenUdpEncapSocketRandomPort() throws Exception {
+ try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+ assertTrue("Returned invalid port", encapSocket.getPort() != 0);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
new file mode 100644
index 0000000..ae38faa
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -0,0 +1,899 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
+import static android.net.IpSecManager.UdpEncapsulationSocket;
+import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
+import static android.net.cts.PacketUtils.BytePayload;
+import static android.net.cts.PacketUtils.EspHeader;
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.IpHeader;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.net.cts.PacketUtils.UdpHeader;
+import static android.net.cts.PacketUtils.getIpHeader;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.cts.PacketUtils.Payload;
+import android.net.cts.util.CtsNetUtils;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
+public class IpSecManagerTunnelTest extends IpSecBaseTest {
+ private static final String TAG = IpSecManagerTunnelTest.class.getSimpleName();
+
+ private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1");
+ private static final InetAddress REMOTE_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.2");
+ private static final InetAddress LOCAL_OUTER_6 =
+ InetAddress.parseNumericAddress("2001:db8:1::1");
+ private static final InetAddress REMOTE_OUTER_6 =
+ InetAddress.parseNumericAddress("2001:db8:1::2");
+
+ private static final InetAddress LOCAL_INNER_4 =
+ InetAddress.parseNumericAddress("198.51.100.1");
+ private static final InetAddress REMOTE_INNER_4 =
+ InetAddress.parseNumericAddress("198.51.100.2");
+ private static final InetAddress LOCAL_INNER_6 =
+ InetAddress.parseNumericAddress("2001:db8:2::1");
+ private static final InetAddress REMOTE_INNER_6 =
+ InetAddress.parseNumericAddress("2001:db8:2::2");
+
+ private static final int IP4_PREFIX_LEN = 32;
+ private static final int IP6_PREFIX_LEN = 128;
+
+ private static final int TIMEOUT_MS = 500;
+
+ // Static state to reduce setup/teardown
+ private static ConnectivityManager sCM;
+ private static TestNetworkManager sTNM;
+ private static ParcelFileDescriptor sTunFd;
+ private static TestNetworkCallback sTunNetworkCallback;
+ private static Network sTunNetwork;
+ private static TunUtils sTunUtils;
+
+ private static Context sContext = InstrumentationRegistry.getContext();
+ private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+ sCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
+
+ // Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and
+ // a standard permission is insufficient. So we shell out the appop, to give us the
+ // right appop permissions.
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
+
+ TestNetworkInterface testIface =
+ sTNM.createTunInterface(
+ new LinkAddress[] {
+ new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
+ new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)
+ });
+
+ sTunFd = testIface.getFileDescriptor();
+ sTunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
+ sTunNetworkCallback.waitForAvailable();
+ sTunNetwork = sTunNetworkCallback.currentNetwork;
+
+ sTunUtils = new TunUtils(sTunFd);
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Set to true before every run; some tests flip this.
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
+
+ // Clear sTunUtils state
+ sTunUtils.reset();
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
+
+ sCM.unregisterNetworkCallback(sTunNetworkCallback);
+
+ sTNM.teardownTestNetwork(sTunNetwork);
+ sTunFd.close();
+
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testSecurityExceptionCreateTunnelInterfaceWithoutAppop() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Ensure we don't have the appop. Permission is not requested in the Manifest
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
+
+ // Security exceptions are thrown regardless of IPv4/IPv6. Just test one
+ try {
+ mISM.createIpSecTunnelInterface(LOCAL_INNER_6, REMOTE_INNER_6, sTunNetwork);
+ fail("Did not throw SecurityException for Tunnel creation without appop");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testSecurityExceptionBuildTunnelTransformWithoutAppop() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Ensure we don't have the appop. Permission is not requested in the Manifest
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
+
+ // Security exceptions are thrown regardless of IPv4/IPv6. Just test one
+ try (IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(LOCAL_INNER_4);
+ IpSecTransform transform =
+ new IpSecTransform.Builder(sContext)
+ .buildTunnelModeTransform(REMOTE_INNER_4, spi)) {
+ fail("Did not throw SecurityException for Transform creation without appop");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ /* Test runnables for callbacks after IPsec tunnels are set up. */
+ private abstract class IpSecTunnelTestRunnable {
+ /**
+ * Runs the test code, and returns the inner socket port, if any.
+ *
+ * @param ipsecNetwork The IPsec Interface based Network for binding sockets on
+ * @return the integer port of the inner socket if outbound, or 0 if inbound
+ * IpSecTunnelTestRunnable
+ * @throws Exception if any part of the test failed.
+ */
+ public abstract int run(Network ipsecNetwork) throws Exception;
+ }
+
+ private int getPacketSize(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
+ int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN;
+
+ // Inner Transport mode packet size
+ if (transportInTunnelMode) {
+ expectedPacketSize =
+ PacketUtils.calculateEspPacketSize(
+ expectedPacketSize,
+ AES_CBC_IV_LEN,
+ AES_CBC_BLK_SIZE,
+ AUTH_KEY.length * 4);
+ }
+
+ // Inner IP Header
+ expectedPacketSize += innerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN;
+
+ // Tunnel mode transform size
+ expectedPacketSize =
+ PacketUtils.calculateEspPacketSize(
+ expectedPacketSize, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, AUTH_KEY.length * 4);
+
+ // UDP encap size
+ expectedPacketSize += useEncap ? UDP_HDRLEN : 0;
+
+ // Outer IP Header
+ expectedPacketSize += outerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN;
+
+ return expectedPacketSize;
+ }
+
+ private interface IpSecTunnelTestRunnableFactory {
+ IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int innerSocketPort,
+ int expectedPacketSize)
+ throws Exception;
+ }
+
+ private class OutputIpSecTunnelTestRunnableFactory implements IpSecTunnelTestRunnableFactory {
+ public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int unusedInnerSocketPort,
+ int expectedPacketSize) {
+ return new IpSecTunnelTestRunnable() {
+ @Override
+ public int run(Network ipsecNetwork) throws Exception {
+ // Build a socket and send traffic
+ JavaUdpSocket socket = new JavaUdpSocket(localInner);
+ ipsecNetwork.bindSocket(socket.mSocket);
+ int innerSocketPort = socket.getPort();
+
+ // For Transport-In-Tunnel mode, apply transform to socket
+ if (transportInTunnelMode) {
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_IN, inTransportTransform);
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_OUT, outTransportTransform);
+ }
+
+ socket.sendTo(TEST_DATA, remoteInner, socket.getPort());
+
+ // Verify that an encrypted packet is sent. As of right now, checking encrypted
+ // body is not possible, due to the test not knowing some of the fields of the
+ // inner IP header (flow label, flags, etc)
+ sTunUtils.awaitEspPacketNoPlaintext(
+ spi, TEST_DATA, encapPort != 0, expectedPacketSize);
+
+ socket.close();
+
+ return innerSocketPort;
+ }
+ };
+ }
+ }
+
+ private class InputReflectedIpSecTunnelTestRunnableFactory
+ implements IpSecTunnelTestRunnableFactory {
+ public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int innerSocketPort,
+ int expectedPacketSize)
+ throws Exception {
+ return new IpSecTunnelTestRunnable() {
+ @Override
+ public int run(Network ipsecNetwork) throws Exception {
+ // Build a socket and receive traffic
+ JavaUdpSocket socket = new JavaUdpSocket(localInner, innerSocketPort);
+ ipsecNetwork.bindSocket(socket.mSocket);
+
+ // For Transport-In-Tunnel mode, apply transform to socket
+ if (transportInTunnelMode) {
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_IN, outTransportTransform);
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform);
+ }
+
+ sTunUtils.reflectPackets();
+
+ // Receive packet from socket, and validate that the payload is correct
+ receiveAndValidatePacket(socket);
+
+ socket.close();
+
+ return 0;
+ }
+ };
+ }
+ }
+
+ private class InputPacketGeneratorIpSecTunnelTestRunnableFactory
+ implements IpSecTunnelTestRunnableFactory {
+ public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int innerSocketPort,
+ int expectedPacketSize)
+ throws Exception {
+ return new IpSecTunnelTestRunnable() {
+ @Override
+ public int run(Network ipsecNetwork) throws Exception {
+ // Build a socket and receive traffic
+ JavaUdpSocket socket = new JavaUdpSocket(localInner);
+ ipsecNetwork.bindSocket(socket.mSocket);
+
+ // For Transport-In-Tunnel mode, apply transform to socket
+ if (transportInTunnelMode) {
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_IN, outTransportTransform);
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform);
+ }
+
+ byte[] pkt;
+ if (transportInTunnelMode) {
+ pkt =
+ getTransportInTunnelModePacket(
+ spi,
+ spi,
+ remoteInner,
+ localInner,
+ remoteOuter,
+ localOuter,
+ socket.getPort(),
+ encapPort);
+ } else {
+ pkt =
+ getTunnelModePacket(
+ spi,
+ remoteInner,
+ localInner,
+ remoteOuter,
+ localOuter,
+ socket.getPort(),
+ encapPort);
+ }
+ sTunUtils.injectPacket(pkt);
+
+ // Receive packet from socket, and validate
+ receiveAndValidatePacket(socket);
+
+ socket.close();
+
+ return 0;
+ }
+ };
+ }
+ }
+
+ private void checkTunnelOutput(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+ throws Exception {
+ checkTunnel(
+ innerFamily,
+ outerFamily,
+ useEncap,
+ transportInTunnelMode,
+ new OutputIpSecTunnelTestRunnableFactory());
+ }
+
+ private void checkTunnelInput(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+ throws Exception {
+ checkTunnel(
+ innerFamily,
+ outerFamily,
+ useEncap,
+ transportInTunnelMode,
+ new InputPacketGeneratorIpSecTunnelTestRunnableFactory());
+ }
+
+ /**
+ * Validates that the kernel can talk to itself.
+ *
+ * <p>This test takes an outbound IPsec packet, reflects it (by flipping IP src/dst), and
+ * injects it back into the TUN. This test then verifies that a packet with the correct payload
+ * is found on the specified socket/port.
+ */
+ public void checkTunnelReflected(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+ throws Exception {
+ InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
+ InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6;
+
+ InetAddress localOuter = outerFamily == AF_INET ? LOCAL_OUTER_4 : LOCAL_OUTER_6;
+ InetAddress remoteOuter = outerFamily == AF_INET ? REMOTE_OUTER_4 : REMOTE_OUTER_6;
+
+ // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels.
+ int spi = getRandomSpi(localOuter, remoteOuter);
+ int expectedPacketSize =
+ getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+
+ try (IpSecManager.SecurityParameterIndex inTransportSpi =
+ mISM.allocateSecurityParameterIndex(localInner, spi);
+ IpSecManager.SecurityParameterIndex outTransportSpi =
+ mISM.allocateSecurityParameterIndex(remoteInner, spi);
+ IpSecTransform inTransportTransform =
+ buildIpSecTransform(sContext, inTransportSpi, null, remoteInner);
+ IpSecTransform outTransportTransform =
+ buildIpSecTransform(sContext, outTransportSpi, null, localInner);
+ UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+
+ // Run output direction tests
+ IpSecTunnelTestRunnable outputIpSecTunnelTestRunnable =
+ new OutputIpSecTunnelTestRunnableFactory()
+ .getIpSecTunnelTestRunnable(
+ transportInTunnelMode,
+ spi,
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ inTransportTransform,
+ outTransportTransform,
+ useEncap ? encapSocket.getPort() : 0,
+ 0,
+ expectedPacketSize);
+ int innerSocketPort =
+ buildTunnelNetworkAndRunTests(
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ useEncap ? encapSocket : null,
+ outputIpSecTunnelTestRunnable);
+
+ // Input direction tests, with matching inner socket ports.
+ IpSecTunnelTestRunnable inputIpSecTunnelTestRunnable =
+ new InputReflectedIpSecTunnelTestRunnableFactory()
+ .getIpSecTunnelTestRunnable(
+ transportInTunnelMode,
+ spi,
+ remoteInner,
+ localInner,
+ localOuter,
+ remoteOuter,
+ inTransportTransform,
+ outTransportTransform,
+ useEncap ? encapSocket.getPort() : 0,
+ innerSocketPort,
+ expectedPacketSize);
+ buildTunnelNetworkAndRunTests(
+ remoteInner,
+ localInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ useEncap ? encapSocket : null,
+ inputIpSecTunnelTestRunnable);
+ }
+ }
+
+ public void checkTunnel(
+ int innerFamily,
+ int outerFamily,
+ boolean useEncap,
+ boolean transportInTunnelMode,
+ IpSecTunnelTestRunnableFactory factory)
+ throws Exception {
+
+ InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
+ InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6;
+
+ InetAddress localOuter = outerFamily == AF_INET ? LOCAL_OUTER_4 : LOCAL_OUTER_6;
+ InetAddress remoteOuter = outerFamily == AF_INET ? REMOTE_OUTER_4 : REMOTE_OUTER_6;
+
+ // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels.
+ // Re-uses the same SPI to ensure that even in cases of symmetric SPIs shared across tunnel
+ // and transport mode, packets are encrypted/decrypted properly based on the src/dst.
+ int spi = getRandomSpi(localOuter, remoteOuter);
+ int expectedPacketSize =
+ getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+
+ try (IpSecManager.SecurityParameterIndex inTransportSpi =
+ mISM.allocateSecurityParameterIndex(localInner, spi);
+ IpSecManager.SecurityParameterIndex outTransportSpi =
+ mISM.allocateSecurityParameterIndex(remoteInner, spi);
+ IpSecTransform inTransportTransform =
+ buildIpSecTransform(sContext, inTransportSpi, null, remoteInner);
+ IpSecTransform outTransportTransform =
+ buildIpSecTransform(sContext, outTransportSpi, null, localInner);
+ UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+
+ buildTunnelNetworkAndRunTests(
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ useEncap ? encapSocket : null,
+ factory.getIpSecTunnelTestRunnable(
+ transportInTunnelMode,
+ spi,
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ inTransportTransform,
+ outTransportTransform,
+ useEncap ? encapSocket.getPort() : 0,
+ 0,
+ expectedPacketSize));
+ }
+ }
+
+ private int buildTunnelNetworkAndRunTests(
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ int spi,
+ UdpEncapsulationSocket encapSocket,
+ IpSecTunnelTestRunnable test)
+ throws Exception {
+ int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN;
+ TestNetworkCallback testNetworkCb = null;
+ int innerSocketPort;
+
+ try (IpSecManager.SecurityParameterIndex inSpi =
+ mISM.allocateSecurityParameterIndex(localOuter, spi);
+ IpSecManager.SecurityParameterIndex outSpi =
+ mISM.allocateSecurityParameterIndex(remoteOuter, spi);
+ IpSecManager.IpSecTunnelInterface tunnelIface =
+ mISM.createIpSecTunnelInterface(localOuter, remoteOuter, sTunNetwork)) {
+ // Build the test network
+ tunnelIface.addAddress(localInner, innerPrefixLen);
+ testNetworkCb = mCtsNetUtils.setupAndGetTestNetwork(tunnelIface.getInterfaceName());
+ testNetworkCb.waitForAvailable();
+ Network testNetwork = testNetworkCb.currentNetwork;
+
+ // Check interface was created
+ assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
+
+ // Verify address was added
+ final NetworkInterface netIface = NetworkInterface.getByInetAddress(localInner);
+ assertNotNull(netIface);
+ assertEquals(tunnelIface.getInterfaceName(), netIface.getDisplayName());
+
+ // Configure Transform parameters
+ IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
+ transformBuilder.setEncryption(
+ new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+ transformBuilder.setAuthentication(
+ new IpSecAlgorithm(
+ IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
+
+ if (encapSocket != null) {
+ transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+ }
+
+ // Apply transform and check that traffic is properly encrypted
+ try (IpSecTransform inTransform =
+ transformBuilder.buildTunnelModeTransform(remoteOuter, inSpi);
+ IpSecTransform outTransform =
+ transformBuilder.buildTunnelModeTransform(localOuter, outSpi)) {
+ mISM.applyTunnelModeTransform(tunnelIface, IpSecManager.DIRECTION_IN, inTransform);
+ mISM.applyTunnelModeTransform(
+ tunnelIface, IpSecManager.DIRECTION_OUT, outTransform);
+
+ innerSocketPort = test.run(testNetwork);
+ }
+
+ // Teardown the test network
+ sTNM.teardownTestNetwork(testNetwork);
+
+ // Remove addresses and check that interface is still present, but fails lookup-by-addr
+ tunnelIface.removeAddress(localInner, innerPrefixLen);
+ assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
+ assertNull(NetworkInterface.getByInetAddress(localInner));
+
+ // Check interface was cleaned up
+ tunnelIface.close();
+ assertNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
+ } finally {
+ if (testNetworkCb != null) {
+ sCM.unregisterNetworkCallback(testNetworkCb);
+ }
+ }
+
+ return innerSocketPort;
+ }
+
+ private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
+ byte[] socketResponseBytes = socket.receive();
+ assertArrayEquals(TEST_DATA, socketResponseBytes);
+ }
+
+ private int getRandomSpi(InetAddress localOuter, InetAddress remoteOuter) throws Exception {
+ // Try to allocate both in and out SPIs using the same requested SPI value.
+ try (IpSecManager.SecurityParameterIndex inSpi =
+ mISM.allocateSecurityParameterIndex(localOuter);
+ IpSecManager.SecurityParameterIndex outSpi =
+ mISM.allocateSecurityParameterIndex(remoteOuter, inSpi.getSpi()); ) {
+ return inSpi.getSpi();
+ }
+ }
+
+ private EspHeader buildTransportModeEspPacket(
+ int spi, InetAddress src, InetAddress dst, int port, Payload payload) throws Exception {
+ IpHeader preEspIpHeader = getIpHeader(payload.getProtocolId(), src, dst, payload);
+
+ return new EspHeader(
+ payload.getProtocolId(),
+ spi,
+ 1, // sequence number
+ CRYPT_KEY, // Same key for auth and crypt
+ payload.getPacketBytes(preEspIpHeader));
+ }
+
+ private EspHeader buildTunnelModeEspPacket(
+ int spi,
+ InetAddress srcInner,
+ InetAddress dstInner,
+ InetAddress srcOuter,
+ InetAddress dstOuter,
+ int port,
+ int encapPort,
+ Payload payload)
+ throws Exception {
+ IpHeader innerIp = getIpHeader(payload.getProtocolId(), srcInner, dstInner, payload);
+ return new EspHeader(
+ innerIp.getProtocolId(),
+ spi,
+ 1, // sequence number
+ CRYPT_KEY, // Same key for auth and crypt
+ innerIp.getPacketBytes());
+ }
+
+ private IpHeader maybeEncapPacket(
+ InetAddress src, InetAddress dst, int encapPort, EspHeader espPayload)
+ throws Exception {
+
+ Payload payload = espPayload;
+ if (encapPort != 0) {
+ payload = new UdpHeader(encapPort, encapPort, espPayload);
+ }
+
+ return getIpHeader(payload.getProtocolId(), src, dst, payload);
+ }
+
+ private byte[] getTunnelModePacket(
+ int spi,
+ InetAddress srcInner,
+ InetAddress dstInner,
+ InetAddress srcOuter,
+ InetAddress dstOuter,
+ int port,
+ int encapPort)
+ throws Exception {
+ UdpHeader udp = new UdpHeader(port, port, new BytePayload(TEST_DATA));
+
+ EspHeader espPayload =
+ buildTunnelModeEspPacket(
+ spi, srcInner, dstInner, srcOuter, dstOuter, port, encapPort, udp);
+ return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes();
+ }
+
+ private byte[] getTransportInTunnelModePacket(
+ int spiInner,
+ int spiOuter,
+ InetAddress srcInner,
+ InetAddress dstInner,
+ InetAddress srcOuter,
+ InetAddress dstOuter,
+ int port,
+ int encapPort)
+ throws Exception {
+ UdpHeader udp = new UdpHeader(port, port, new BytePayload(TEST_DATA));
+
+ EspHeader espPayload = buildTransportModeEspPacket(spiInner, srcInner, dstInner, port, udp);
+ espPayload =
+ buildTunnelModeEspPacket(
+ spiOuter,
+ srcInner,
+ dstInner,
+ srcOuter,
+ dstOuter,
+ port,
+ encapPort,
+ espPayload);
+ return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes();
+ }
+
+ // Transport-in-Tunnel mode tests
+ @Test
+ public void testTransportInTunnelModeV4InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, false, true);
+ checkTunnelInput(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, true, true);
+ checkTunnelInput(AF_INET, AF_INET, true, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET6, false, true);
+ checkTunnelInput(AF_INET, AF_INET6, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, false, true);
+ checkTunnelInput(AF_INET6, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, true, true);
+ checkTunnelInput(AF_INET6, AF_INET, true, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET6, false, true);
+ checkTunnelInput(AF_INET, AF_INET6, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ // Tunnel mode tests
+ @Test
+ public void testTunnelV4InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, false, false);
+ checkTunnelInput(AF_INET, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV4InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV4InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, true, false);
+ checkTunnelInput(AF_INET, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV4InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV4InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET6, false, false);
+ checkTunnelInput(AF_INET, AF_INET6, false, false);
+ }
+
+ @Test
+ public void testTunnelV4InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET6, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, false, false);
+ checkTunnelInput(AF_INET6, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET6, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, true, false);
+ checkTunnelInput(AF_INET6, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET6, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV6InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET6, false, false);
+ checkTunnelInput(AF_INET6, AF_INET6, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET6, AF_INET6, false, false);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java b/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java
new file mode 100644
index 0000000..7c5a1b3
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+
+public class LocalServerSocketTest extends TestCase {
+
+ public void testLocalServerSocket() throws IOException {
+ String address = "com.android.net.LocalServerSocketTest_testLocalServerSocket";
+ LocalServerSocket localServerSocket = new LocalServerSocket(address);
+ assertNotNull(localServerSocket.getLocalSocketAddress());
+
+ // create client socket
+ LocalSocket clientSocket = new LocalSocket();
+
+ // establish connection between client and server
+ clientSocket.connect(new LocalSocketAddress(address));
+ LocalSocket serverSocket = localServerSocket.accept();
+
+ assertTrue(serverSocket.isConnected());
+ assertTrue(serverSocket.isBound());
+
+ // send data from client to server
+ OutputStream clientOutStream = clientSocket.getOutputStream();
+ clientOutStream.write(12);
+ InputStream serverInStream = serverSocket.getInputStream();
+ assertEquals(12, serverInStream.read());
+
+ // send data from server to client
+ OutputStream serverOutStream = serverSocket.getOutputStream();
+ serverOutStream.write(3);
+ InputStream clientInStream = clientSocket.getInputStream();
+ assertEquals(3, clientInStream.read());
+
+ // close server socket
+ assertNotNull(localServerSocket.getFileDescriptor());
+ localServerSocket.close();
+ assertNull(localServerSocket.getFileDescriptor());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java b/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java
new file mode 100644
index 0000000..6ef003b
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.LocalSocketAddress;
+import android.net.LocalSocketAddress.Namespace;
+import android.test.AndroidTestCase;
+
+public class LocalSocketAddressTest extends AndroidTestCase {
+
+ public void testNewLocalSocketAddressWithDefaultNamespace() {
+ // default namespace
+ LocalSocketAddress localSocketAddress = new LocalSocketAddress("name");
+ assertEquals("name", localSocketAddress.getName());
+ assertEquals(Namespace.ABSTRACT, localSocketAddress.getNamespace());
+
+ // specify the namespace
+ LocalSocketAddress localSocketAddress2 =
+ new LocalSocketAddress("name2", Namespace.ABSTRACT);
+ assertEquals("name2", localSocketAddress2.getName());
+ assertEquals(Namespace.ABSTRACT, localSocketAddress2.getNamespace());
+
+ LocalSocketAddress localSocketAddress3 =
+ new LocalSocketAddress("name3", Namespace.FILESYSTEM);
+ assertEquals("name3", localSocketAddress3.getName());
+ assertEquals(Namespace.FILESYSTEM, localSocketAddress3.getNamespace());
+
+ LocalSocketAddress localSocketAddress4 =
+ new LocalSocketAddress("name4", Namespace.RESERVED);
+ assertEquals("name4", localSocketAddress4.getName());
+ assertEquals(Namespace.RESERVED, localSocketAddress4.getNamespace());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java b/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java
new file mode 100644
index 0000000..97dfa43
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.LocalSocketAddress.Namespace;
+import android.test.AndroidTestCase;
+
+public class LocalSocketAddress_NamespaceTest extends AndroidTestCase {
+
+ public void testValueOf() {
+ assertEquals(Namespace.ABSTRACT, Namespace.valueOf("ABSTRACT"));
+ assertEquals(Namespace.RESERVED, Namespace.valueOf("RESERVED"));
+ assertEquals(Namespace.FILESYSTEM, Namespace.valueOf("FILESYSTEM"));
+ }
+
+ public void testValues() {
+ Namespace[] expected = Namespace.values();
+ assertEquals(Namespace.ABSTRACT, expected[0]);
+ assertEquals(Namespace.RESERVED, expected[1]);
+ assertEquals(Namespace.FILESYSTEM, expected[2]);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketTest.java b/tests/cts/net/src/android/net/cts/LocalSocketTest.java
new file mode 100644
index 0000000..6e61705
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalSocketTest.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import junit.framework.TestCase;
+
+import android.net.Credentials;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class LocalSocketTest extends TestCase {
+ private final static String ADDRESS_PREFIX = "com.android.net.LocalSocketTest";
+
+ public void testLocalConnections() throws IOException {
+ String address = ADDRESS_PREFIX + "_testLocalConnections";
+ // create client and server socket
+ LocalServerSocket localServerSocket = new LocalServerSocket(address);
+ LocalSocket clientSocket = new LocalSocket();
+
+ // establish connection between client and server
+ LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
+ assertFalse(clientSocket.isConnected());
+ clientSocket.connect(locSockAddr);
+ assertTrue(clientSocket.isConnected());
+
+ LocalSocket serverSocket = localServerSocket.accept();
+ assertTrue(serverSocket.isConnected());
+ assertTrue(serverSocket.isBound());
+ try {
+ serverSocket.bind(localServerSocket.getLocalSocketAddress());
+ fail("Cannot bind a LocalSocket from accept()");
+ } catch (IOException expected) {
+ }
+ try {
+ serverSocket.connect(locSockAddr);
+ fail("Cannot connect a LocalSocket from accept()");
+ } catch (IOException expected) {
+ }
+
+ Credentials credent = clientSocket.getPeerCredentials();
+ assertTrue(0 != credent.getPid());
+
+ // send data from client to server
+ OutputStream clientOutStream = clientSocket.getOutputStream();
+ clientOutStream.write(12);
+ InputStream serverInStream = serverSocket.getInputStream();
+ assertEquals(12, serverInStream.read());
+
+ //send data from server to client
+ OutputStream serverOutStream = serverSocket.getOutputStream();
+ serverOutStream.write(3);
+ InputStream clientInStream = clientSocket.getInputStream();
+ assertEquals(3, clientInStream.read());
+
+ // Test sending and receiving file descriptors
+ clientSocket.setFileDescriptorsForSend(new FileDescriptor[]{FileDescriptor.in});
+ clientOutStream.write(32);
+ assertEquals(32, serverInStream.read());
+
+ FileDescriptor[] out = serverSocket.getAncillaryFileDescriptors();
+ assertEquals(1, out.length);
+ FileDescriptor fd = clientSocket.getFileDescriptor();
+ assertTrue(fd.valid());
+
+ //shutdown input stream of client
+ clientSocket.shutdownInput();
+ assertEquals(-1, clientInStream.read());
+
+ //shutdown output stream of client
+ clientSocket.shutdownOutput();
+ try {
+ clientOutStream.write(10);
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+
+ //shutdown input stream of server
+ serverSocket.shutdownInput();
+ assertEquals(-1, serverInStream.read());
+
+ //shutdown output stream of server
+ serverSocket.shutdownOutput();
+ try {
+ serverOutStream.write(10);
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+
+ //close client socket
+ clientSocket.close();
+ try {
+ clientInStream.read();
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+
+ //close server socket
+ serverSocket.close();
+ try {
+ serverInStream.read();
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+ }
+
+ public void testAccessors() throws IOException {
+ String address = ADDRESS_PREFIX + "_testAccessors";
+ LocalSocket socket = new LocalSocket();
+ LocalSocketAddress addr = new LocalSocketAddress(address);
+
+ assertFalse(socket.isBound());
+ socket.bind(addr);
+ assertTrue(socket.isBound());
+ assertEquals(addr, socket.getLocalSocketAddress());
+
+ String str = socket.toString();
+ assertTrue(str.contains("impl:android.net.LocalSocketImpl"));
+
+ socket.setReceiveBufferSize(1999);
+ assertEquals(1999 << 1, socket.getReceiveBufferSize());
+
+ socket.setSendBufferSize(3998);
+ assertEquals(3998 << 1, socket.getSendBufferSize());
+
+ assertEquals(0, socket.getSoTimeout());
+ socket.setSoTimeout(1996);
+ assertTrue(socket.getSoTimeout() > 0);
+
+ try {
+ socket.getRemoteSocketAddress();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.isClosed();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.isInputShutdown();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.isOutputShutdown();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.connect(addr, 2005);
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ socket.close();
+ }
+
+ // http://b/31205169
+ public void testSetSoTimeout_readTimeout() throws Exception {
+ String address = ADDRESS_PREFIX + "_testSetSoTimeout_readTimeout";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ final LocalSocket clientSocket = socketPair.clientSocket;
+
+ // Set the timeout in millis.
+ int timeoutMillis = 1000;
+ clientSocket.setSoTimeout(timeoutMillis);
+
+ // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
+ Callable<Result> reader = () -> {
+ try {
+ clientSocket.getInputStream().read();
+ return Result.noException("Did not block");
+ } catch (IOException e) {
+ return Result.exception(e);
+ }
+ };
+ // Allow the configured timeout, plus some slop.
+ int allowedTime = timeoutMillis + 2000;
+ Result result = runInSeparateThread(allowedTime, reader);
+
+ // Check the message was a timeout, it's all we have to go on.
+ String expectedMessage = Os.strerror(OsConstants.EAGAIN);
+ result.assertThrewIOException(expectedMessage);
+ }
+ }
+
+ // http://b/31205169
+ public void testSetSoTimeout_writeTimeout() throws Exception {
+ String address = ADDRESS_PREFIX + "_testSetSoTimeout_writeTimeout";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ final LocalSocket clientSocket = socketPair.clientSocket;
+
+ // Set the timeout in millis.
+ int timeoutMillis = 1000;
+ clientSocket.setSoTimeout(timeoutMillis);
+
+ // Set a small buffer size so we know we can flood it.
+ clientSocket.setSendBufferSize(100);
+ final int bufferSize = clientSocket.getSendBufferSize();
+
+ // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
+ Callable<Result> writer = () -> {
+ try {
+ byte[] toWrite = new byte[bufferSize * 2];
+ clientSocket.getOutputStream().write(toWrite);
+ return Result.noException("Did not block");
+ } catch (IOException e) {
+ return Result.exception(e);
+ }
+ };
+ // Allow the configured timeout, plus some slop.
+ int allowedTime = timeoutMillis + 2000;
+
+ Result result = runInSeparateThread(allowedTime, writer);
+
+ // Check the message was a timeout, it's all we have to go on.
+ String expectedMessage = Os.strerror(OsConstants.EAGAIN);
+ result.assertThrewIOException(expectedMessage);
+ }
+ }
+
+ public void testAvailable() throws Exception {
+ String address = ADDRESS_PREFIX + "_testAvailable";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ LocalSocket clientSocket = socketPair.clientSocket;
+ LocalSocket serverSocket = socketPair.serverSocket.accept();
+
+ OutputStream clientOutputStream = clientSocket.getOutputStream();
+ InputStream serverInputStream = serverSocket.getInputStream();
+ assertEquals(0, serverInputStream.available());
+
+ byte[] buffer = new byte[50];
+ clientOutputStream.write(buffer);
+ assertEquals(50, serverInputStream.available());
+
+ InputStream clientInputStream = clientSocket.getInputStream();
+ OutputStream serverOutputStream = serverSocket.getOutputStream();
+ assertEquals(0, clientInputStream.available());
+ serverOutputStream.write(buffer);
+ assertEquals(50, serverInputStream.available());
+
+ serverSocket.close();
+ }
+ }
+
+ // http://b/34095140
+ public void testLocalSocketCreatedFromFileDescriptor() throws Exception {
+ String address = ADDRESS_PREFIX + "_testLocalSocketCreatedFromFileDescriptor";
+
+ // Establish connection between a local client and server to get a valid client socket file
+ // descriptor.
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ // Extract the client FileDescriptor we can use.
+ FileDescriptor fileDescriptor = socketPair.clientSocket.getFileDescriptor();
+ assertTrue(fileDescriptor.valid());
+
+ // Create the LocalSocket we want to test.
+ LocalSocket clientSocketCreatedFromFileDescriptor =
+ LocalSocket.createConnectedLocalSocket(fileDescriptor);
+ assertTrue(clientSocketCreatedFromFileDescriptor.isConnected());
+ assertTrue(clientSocketCreatedFromFileDescriptor.isBound());
+
+ // Test the LocalSocket can be used for communication.
+ LocalSocket serverSocket = socketPair.serverSocket.accept();
+ OutputStream clientOutputStream =
+ clientSocketCreatedFromFileDescriptor.getOutputStream();
+ InputStream serverInputStream = serverSocket.getInputStream();
+
+ clientOutputStream.write(12);
+ assertEquals(12, serverInputStream.read());
+
+ // Closing clientSocketCreatedFromFileDescriptor does not close the file descriptor.
+ clientSocketCreatedFromFileDescriptor.close();
+ assertTrue(fileDescriptor.valid());
+
+ // .. while closing the LocalSocket that owned the file descriptor does.
+ socketPair.clientSocket.close();
+ assertFalse(fileDescriptor.valid());
+ }
+ }
+
+ public void testFlush() throws Exception {
+ String address = ADDRESS_PREFIX + "_testFlush";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ LocalSocket clientSocket = socketPair.clientSocket;
+ LocalSocket serverSocket = socketPair.serverSocket.accept();
+
+ OutputStream clientOutputStream = clientSocket.getOutputStream();
+ InputStream serverInputStream = serverSocket.getInputStream();
+ testFlushWorks(clientOutputStream, serverInputStream);
+
+ OutputStream serverOutputStream = serverSocket.getOutputStream();
+ InputStream clientInputStream = clientSocket.getInputStream();
+ testFlushWorks(serverOutputStream, clientInputStream);
+
+ serverSocket.close();
+ }
+ }
+
+ private void testFlushWorks(OutputStream outputStream, InputStream inputStream)
+ throws Exception {
+ final int bytesToTransfer = 50;
+ StreamReader inputStreamReader = new StreamReader(inputStream, bytesToTransfer);
+
+ byte[] buffer = new byte[bytesToTransfer];
+ outputStream.write(buffer);
+ assertEquals(bytesToTransfer, inputStream.available());
+
+ // Start consuming the data.
+ inputStreamReader.start();
+
+ // This doesn't actually flush any buffers, it just polls until the reader has read all the
+ // bytes.
+ outputStream.flush();
+
+ inputStreamReader.waitForCompletion(5000);
+ inputStreamReader.assertBytesRead(bytesToTransfer);
+ assertEquals(0, inputStream.available());
+ }
+
+ private static class StreamReader extends Thread {
+ private final InputStream is;
+ private final int expectedByteCount;
+ private final CountDownLatch completeLatch = new CountDownLatch(1);
+
+ private volatile Exception exception;
+ private int bytesRead;
+
+ private StreamReader(InputStream is, int expectedByteCount) {
+ this.is = is;
+ this.expectedByteCount = expectedByteCount;
+ }
+
+ @Override
+ public void run() {
+ try {
+ byte[] buffer = new byte[10];
+ int readCount;
+ while ((readCount = is.read(buffer)) >= 0) {
+ bytesRead += readCount;
+ if (bytesRead >= expectedByteCount) {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ exception = e;
+ } finally {
+ completeLatch.countDown();
+ }
+ }
+
+ public void waitForCompletion(long waitMillis) throws Exception {
+ if (!completeLatch.await(waitMillis, TimeUnit.MILLISECONDS)) {
+ fail("Timeout waiting for completion");
+ }
+ if (exception != null) {
+ throw new Exception("Read failed", exception);
+ }
+ }
+
+ public void assertBytesRead(int expected) {
+ assertEquals(expected, bytesRead);
+ }
+ }
+
+ private static class Result {
+ private final String type;
+ private final Exception e;
+
+ private Result(String type, Exception e) {
+ this.type = type;
+ this.e = e;
+ }
+
+ static Result noException(String description) {
+ return new Result(description, null);
+ }
+
+ static Result exception(Exception e) {
+ return new Result(e.getClass().getName(), e);
+ }
+
+ void assertThrewIOException(String expectedMessage) {
+ assertEquals("Unexpected result type", IOException.class.getName(), type);
+ assertEquals("Unexpected exception message", expectedMessage, e.getMessage());
+ }
+ }
+
+ private static Result runInSeparateThread(int allowedTime, final Callable<Result> callable)
+ throws Exception {
+ ExecutorService service = Executors.newSingleThreadScheduledExecutor();
+ Future<Result> future = service.submit(callable);
+ Result result = future.get(allowedTime, TimeUnit.MILLISECONDS);
+ if (!future.isDone()) {
+ fail("Worker thread appears blocked");
+ }
+ return result;
+ }
+
+ private static class LocalSocketPair implements AutoCloseable {
+ static LocalSocketPair createConnectedSocketPair(String address) throws Exception {
+ LocalServerSocket localServerSocket = new LocalServerSocket(address);
+ final LocalSocket clientSocket = new LocalSocket();
+
+ // Establish connection between client and server
+ LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
+ clientSocket.connect(locSockAddr);
+ assertTrue(clientSocket.isConnected());
+ return new LocalSocketPair(localServerSocket, clientSocket);
+ }
+
+ final LocalServerSocket serverSocket;
+ final LocalSocket clientSocket;
+
+ LocalSocketPair(LocalServerSocket serverSocket, LocalSocket clientSocket) {
+ this.serverSocket = serverSocket;
+ this.clientSocket = clientSocket;
+ }
+
+ public void close() throws Exception {
+ serverSocket.close();
+ clientSocket.close();
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MacAddressTest.java b/tests/cts/net/src/android/net/cts/MacAddressTest.java
new file mode 100644
index 0000000..3fd3bba
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MacAddressTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.MacAddress.TYPE_BROADCAST;
+import static android.net.MacAddress.TYPE_MULTICAST;
+import static android.net.MacAddress.TYPE_UNICAST;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MacAddressTest {
+
+ static class TestCase {
+ final String macAddress;
+ final String ouiString;
+ final int addressType;
+ final boolean isLocallyAssigned;
+
+ TestCase(String macAddress, String ouiString, int addressType, boolean isLocallyAssigned) {
+ this.macAddress = macAddress;
+ this.ouiString = ouiString;
+ this.addressType = addressType;
+ this.isLocallyAssigned = isLocallyAssigned;
+ }
+ }
+
+ static final boolean LOCALLY_ASSIGNED = true;
+ static final boolean GLOBALLY_UNIQUE = false;
+
+ static String typeToString(int addressType) {
+ switch (addressType) {
+ case TYPE_UNICAST:
+ return "TYPE_UNICAST";
+ case TYPE_BROADCAST:
+ return "TYPE_BROADCAST";
+ case TYPE_MULTICAST:
+ return "TYPE_MULTICAST";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ static String localAssignedToString(boolean isLocallyAssigned) {
+ return isLocallyAssigned ? "LOCALLY_ASSIGNED" : "GLOBALLY_UNIQUE";
+ }
+
+ @Test
+ public void testMacAddress() {
+ TestCase[] tests = {
+ new TestCase("ff:ff:ff:ff:ff:ff", "ff:ff:ff", TYPE_BROADCAST, LOCALLY_ASSIGNED),
+ new TestCase("d2:c4:22:4d:32:a8", "d2:c4:22", TYPE_UNICAST, LOCALLY_ASSIGNED),
+ new TestCase("33:33:aa:bb:cc:dd", "33:33:aa", TYPE_MULTICAST, LOCALLY_ASSIGNED),
+ new TestCase("06:00:00:00:00:00", "06:00:00", TYPE_UNICAST, LOCALLY_ASSIGNED),
+ new TestCase("07:00:d3:56:8a:c4", "07:00:d3", TYPE_MULTICAST, LOCALLY_ASSIGNED),
+ new TestCase("00:01:44:55:66:77", "00:01:44", TYPE_UNICAST, GLOBALLY_UNIQUE),
+ new TestCase("08:00:22:33:44:55", "08:00:22", TYPE_UNICAST, GLOBALLY_UNIQUE),
+ };
+
+ for (TestCase tc : tests) {
+ MacAddress mac = MacAddress.fromString(tc.macAddress);
+
+ if (!tc.ouiString.equals(mac.toOuiString())) {
+ fail(String.format("expected OUI string %s, got %s",
+ tc.ouiString, mac.toOuiString()));
+ }
+
+ if (tc.isLocallyAssigned != mac.isLocallyAssigned()) {
+ fail(String.format("expected %s to be %s, got %s", mac,
+ localAssignedToString(tc.isLocallyAssigned),
+ localAssignedToString(mac.isLocallyAssigned())));
+ }
+
+ if (tc.addressType != mac.getAddressType()) {
+ fail(String.format("expected %s address type to be %s, got %s", mac,
+ typeToString(tc.addressType), typeToString(mac.getAddressType())));
+ }
+
+ if (!tc.macAddress.equals(mac.toString())) {
+ fail(String.format("expected toString() to return %s, got %s",
+ tc.macAddress, mac.toString()));
+ }
+
+ if (!mac.equals(MacAddress.fromBytes(mac.toByteArray()))) {
+ byte[] bytes = mac.toByteArray();
+ fail(String.format("expected mac address from bytes %s to be %s, got %s",
+ Arrays.toString(bytes),
+ MacAddress.fromBytes(bytes),
+ mac));
+ }
+ }
+ }
+
+ @Test
+ public void testConstructorInputValidation() {
+ String[] invalidStringAddresses = {
+ "",
+ "abcd",
+ "1:2:3:4:5",
+ "1:2:3:4:5:6:7",
+ "10000:2:3:4:5:6",
+ };
+
+ for (String s : invalidStringAddresses) {
+ try {
+ MacAddress mac = MacAddress.fromString(s);
+ fail("MacAddress.fromString(" + s + ") should have failed, but returned " + mac);
+ } catch (IllegalArgumentException excepted) {
+ }
+ }
+
+ try {
+ MacAddress mac = MacAddress.fromString(null);
+ fail("MacAddress.fromString(null) should have failed, but returned " + mac);
+ } catch (NullPointerException excepted) {
+ }
+
+ byte[][] invalidBytesAddresses = {
+ {},
+ {1,2,3,4,5},
+ {1,2,3,4,5,6,7},
+ };
+
+ for (byte[] b : invalidBytesAddresses) {
+ try {
+ MacAddress mac = MacAddress.fromBytes(b);
+ fail("MacAddress.fromBytes(" + Arrays.toString(b)
+ + ") should have failed, but returned " + mac);
+ } catch (IllegalArgumentException excepted) {
+ }
+ }
+
+ try {
+ MacAddress mac = MacAddress.fromBytes(null);
+ fail("MacAddress.fromBytes(null) should have failed, but returned " + mac);
+ } catch (NullPointerException excepted) {
+ }
+ }
+
+ @Test
+ public void testMatches() {
+ // match 4 bytes prefix
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:00:00"),
+ MacAddress.fromString("ff:ff:ff:ff:00:00")));
+
+ // match bytes 0,1,2 and 5
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:00:00:11"),
+ MacAddress.fromString("ff:ff:ff:00:00:ff")));
+
+ // match 34 bit prefix
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:c0:00"),
+ MacAddress.fromString("ff:ff:ff:ff:c0:00")));
+
+ // fail to match 36 bit prefix
+ assertFalse(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:40:00"),
+ MacAddress.fromString("ff:ff:ff:ff:f0:00")));
+
+ // match all 6 bytes
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:ee:11"),
+ MacAddress.fromString("ff:ff:ff:ff:ff:ff")));
+
+ // match none of 6 bytes
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("00:00:00:00:00:00"),
+ MacAddress.fromString("00:00:00:00:00:00")));
+ }
+
+ /**
+ * Tests that link-local address generation from MAC is valid.
+ */
+ @Test
+ public void testLinkLocalFromMacGeneration() {
+ final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f");
+ final byte[] inet6ll = {(byte) 0xfe, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50,
+ 0x74, (byte) 0xf2, (byte) 0xff, (byte) 0xfe, (byte) 0xb1, (byte) 0xa8, 0x7f};
+ final Inet6Address llv6 = mac.getLinkLocalIpv6FromEui48Mac();
+ assertTrue(llv6.isLinkLocalAddress());
+ assertArrayEquals(inet6ll, llv6.getAddress());
+ }
+
+ @Test
+ public void testParcelMacAddress() {
+ final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f");
+
+ assertParcelSane(mac, 1);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MailToTest.java b/tests/cts/net/src/android/net/cts/MailToTest.java
new file mode 100644
index 0000000..e454d20
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MailToTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.MailTo;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+public class MailToTest extends AndroidTestCase {
+ private static final String MAILTOURI_1 = "mailto:chris@example.com";
+ private static final String MAILTOURI_2 = "mailto:infobot@example.com?subject=current-issue";
+ private static final String MAILTOURI_3 =
+ "mailto:infobot@example.com?body=send%20current-issue";
+ private static final String MAILTOURI_4 = "mailto:infobot@example.com?body=send%20current-" +
+ "issue%0D%0Asend%20index";
+ private static final String MAILTOURI_5 = "mailto:joe@example.com?" +
+ "cc=bob@example.com&body=hello";
+ private static final String MAILTOURI_6 = "mailto:?to=joe@example.com&" +
+ "cc=bob@example.com&body=hello";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void testParseMailToURI() {
+ assertFalse(MailTo.isMailTo(null));
+ assertFalse(MailTo.isMailTo(""));
+ assertFalse(MailTo.isMailTo("http://www.google.com"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_1));
+ MailTo mailTo_1 = MailTo.parse(MAILTOURI_1);
+ Log.d("Trace", mailTo_1.toString());
+ assertEquals("chris@example.com", mailTo_1.getTo());
+ assertEquals(1, mailTo_1.getHeaders().size());
+ assertNull(mailTo_1.getBody());
+ assertNull(mailTo_1.getCc());
+ assertNull(mailTo_1.getSubject());
+ assertEquals("mailto:?to=chris%40example.com&", mailTo_1.toString());
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_2));
+ MailTo mailTo_2 = MailTo.parse(MAILTOURI_2);
+ Log.d("Trace", mailTo_2.toString());
+ assertEquals(2, mailTo_2.getHeaders().size());
+ assertEquals("infobot@example.com", mailTo_2.getTo());
+ assertEquals("current-issue", mailTo_2.getSubject());
+ assertNull(mailTo_2.getBody());
+ assertNull(mailTo_2.getCc());
+ String stringUrl = mailTo_2.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("to=infobot%40example.com&"));
+ assertTrue(stringUrl.contains("subject=current-issue&"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_3));
+ MailTo mailTo_3 = MailTo.parse(MAILTOURI_3);
+ Log.d("Trace", mailTo_3.toString());
+ assertEquals(2, mailTo_3.getHeaders().size());
+ assertEquals("infobot@example.com", mailTo_3.getTo());
+ assertEquals("send current-issue", mailTo_3.getBody());
+ assertNull(mailTo_3.getCc());
+ assertNull(mailTo_3.getSubject());
+ stringUrl = mailTo_3.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("to=infobot%40example.com&"));
+ assertTrue(stringUrl.contains("body=send%20current-issue&"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_4));
+ MailTo mailTo_4 = MailTo.parse(MAILTOURI_4);
+ Log.d("Trace", mailTo_4.toString() + " " + mailTo_4.getBody());
+ assertEquals(2, mailTo_4.getHeaders().size());
+ assertEquals("infobot@example.com", mailTo_4.getTo());
+ assertEquals("send current-issue\r\nsend index", mailTo_4.getBody());
+ assertNull(mailTo_4.getCc());
+ assertNull(mailTo_4.getSubject());
+ stringUrl = mailTo_4.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("to=infobot%40example.com&"));
+ assertTrue(stringUrl.contains("body=send%20current-issue%0D%0Asend%20index&"));
+
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_5));
+ MailTo mailTo_5 = MailTo.parse(MAILTOURI_5);
+ Log.d("Trace", mailTo_5.toString() + mailTo_5.getHeaders().toString()
+ + mailTo_5.getHeaders().size());
+ assertEquals(3, mailTo_5.getHeaders().size());
+ assertEquals("joe@example.com", mailTo_5.getTo());
+ assertEquals("bob@example.com", mailTo_5.getCc());
+ assertEquals("hello", mailTo_5.getBody());
+ assertNull(mailTo_5.getSubject());
+ stringUrl = mailTo_5.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("cc=bob%40example.com&"));
+ assertTrue(stringUrl.contains("body=hello&"));
+ assertTrue(stringUrl.contains("to=joe%40example.com&"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_6));
+ MailTo mailTo_6 = MailTo.parse(MAILTOURI_6);
+ Log.d("Trace", mailTo_6.toString() + mailTo_6.getHeaders().toString()
+ + mailTo_6.getHeaders().size());
+ assertEquals(3, mailTo_6.getHeaders().size());
+ assertEquals(", joe@example.com", mailTo_6.getTo());
+ assertEquals("bob@example.com", mailTo_6.getCc());
+ assertEquals("hello", mailTo_6.getBody());
+ assertNull(mailTo_6.getSubject());
+ stringUrl = mailTo_6.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("cc=bob%40example.com&"));
+ assertTrue(stringUrl.contains("body=hello&"));
+ assertTrue(stringUrl.contains("to=%2C%20joe%40example.com&"));
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
new file mode 100644
index 0000000..691ab99
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkUtils;
+import android.net.cts.util.CtsNetUtils;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.test.AndroidTestCase;
+
+import java.util.ArrayList;
+
+public class MultinetworkApiTest extends AndroidTestCase {
+
+ static {
+ System.loadLibrary("nativemultinetwork_jni");
+ }
+
+ private static final String TAG = "MultinetworkNativeApiTest";
+ static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+
+ /**
+ * @return 0 on success
+ */
+ private static native int runGetaddrinfoCheck(long networkHandle);
+ private static native int runSetprocnetwork(long networkHandle);
+ private static native int runSetsocknetwork(long networkHandle);
+ private static native int runDatagramCheck(long networkHandle);
+ private static native void runResNapiMalformedCheck(long networkHandle);
+ private static native void runResNcancelCheck(long networkHandle);
+ private static native void runResNqueryCheck(long networkHandle);
+ private static native void runResNsendCheck(long networkHandle);
+ private static native void runResNnxDomainCheck(long networkHandle);
+
+
+ private ContentResolver mCR;
+ private ConnectivityManager mCM;
+ private CtsNetUtils mCtsNetUtils;
+ private String mOldMode;
+ private String mOldDnsSpecifier;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCR = getContext().getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(getContext());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private Network[] getTestableNetworks() {
+ final ArrayList<Network> testableNetworks = new ArrayList<Network>();
+ for (Network network : mCM.getAllNetworks()) {
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ if (nc != null
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+ testableNetworks.add(network);
+ }
+ }
+
+ assertTrue(
+ "This test requires that at least one network be connected. " +
+ "Please ensure that the device is connected to a network.",
+ testableNetworks.size() >= 1);
+ return testableNetworks.toArray(new Network[0]);
+ }
+
+ public void testGetaddrinfo() throws ErrnoException {
+ for (Network network : getTestableNetworks()) {
+ int errno = runGetaddrinfoCheck(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "getaddrinfo on " + mCM.getNetworkInfo(network), -errno);
+ }
+ }
+ }
+
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ public void testSetprocnetwork() throws ErrnoException {
+ // Hopefully no prior test in this process space has set a default network.
+ assertNull(mCM.getProcessDefaultNetwork());
+ assertEquals(0, NetworkUtils.getBoundNetworkForProcess());
+
+ for (Network network : getTestableNetworks()) {
+ mCM.setProcessDefaultNetwork(null);
+ assertNull(mCM.getProcessDefaultNetwork());
+
+ int errno = runSetprocnetwork(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "setprocnetwork on " + mCM.getNetworkInfo(network), -errno);
+ }
+ Network processDefault = mCM.getProcessDefaultNetwork();
+ assertNotNull(processDefault);
+ assertEquals(network, processDefault);
+ // TODO: open DatagramSockets, connect them to 192.0.2.1 and 2001:db8::,
+ // and ensure that the source address is in fact on this network as
+ // determined by mCM.getLinkProperties(network).
+
+ mCM.setProcessDefaultNetwork(null);
+ }
+
+ for (Network network : getTestableNetworks()) {
+ NetworkUtils.bindProcessToNetwork(0);
+ assertNull(mCM.getBoundNetworkForProcess());
+
+ int errno = runSetprocnetwork(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "setprocnetwork on " + mCM.getNetworkInfo(network), -errno);
+ }
+ assertEquals(network, new Network(mCM.getBoundNetworkForProcess()));
+ // TODO: open DatagramSockets, connect them to 192.0.2.1 and 2001:db8::,
+ // and ensure that the source address is in fact on this network as
+ // determined by mCM.getLinkProperties(network).
+
+ NetworkUtils.bindProcessToNetwork(0);
+ }
+ }
+
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ public void testSetsocknetwork() throws ErrnoException {
+ for (Network network : getTestableNetworks()) {
+ int errno = runSetsocknetwork(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "setsocknetwork on " + mCM.getNetworkInfo(network), -errno);
+ }
+ }
+ }
+
+ public void testNativeDatagramTransmission() throws ErrnoException {
+ for (Network network : getTestableNetworks()) {
+ int errno = runDatagramCheck(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "DatagramCheck on " + mCM.getNetworkInfo(network), -errno);
+ }
+ }
+ }
+
+ public void testNoSuchNetwork() {
+ final Network eNoNet = new Network(54321);
+ assertNull(mCM.getNetworkInfo(eNoNet));
+
+ final long eNoNetHandle = eNoNet.getNetworkHandle();
+ assertEquals(-OsConstants.ENONET, runSetsocknetwork(eNoNetHandle));
+ assertEquals(-OsConstants.ENONET, runSetprocnetwork(eNoNetHandle));
+ // TODO: correct test permissions so this call is not silently re-mapped
+ // to query on the default network.
+ // assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle));
+ }
+
+ public void testNetworkHandle() {
+ // Test Network -> NetworkHandle -> Network results in the same Network.
+ for (Network network : getTestableNetworks()) {
+ long networkHandle = network.getNetworkHandle();
+ Network newNetwork = Network.fromNetworkHandle(networkHandle);
+ assertEquals(newNetwork, network);
+ }
+
+ // Test that only obfuscated handles are allowed.
+ try {
+ Network.fromNetworkHandle(100);
+ fail();
+ } catch (IllegalArgumentException e) {}
+ try {
+ Network.fromNetworkHandle(-1);
+ fail();
+ } catch (IllegalArgumentException e) {}
+ try {
+ Network.fromNetworkHandle(0);
+ fail();
+ } catch (IllegalArgumentException e) {}
+ }
+
+ public void testResNApi() throws Exception {
+ final Network[] testNetworks = getTestableNetworks();
+
+ for (Network network : testNetworks) {
+ // Throws AssertionError directly in jni function if test fail.
+ runResNqueryCheck(network.getNetworkHandle());
+ runResNsendCheck(network.getNetworkHandle());
+ runResNcancelCheck(network.getNetworkHandle());
+ runResNapiMalformedCheck(network.getNetworkHandle());
+
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ // Some cellular networks configure their DNS servers never to return NXDOMAIN, so don't
+ // test NXDOMAIN on these DNS servers.
+ // b/144521720
+ if (nc != null && !nc.hasTransport(TRANSPORT_CELLULAR)) {
+ runResNnxDomainCheck(network.getNetworkHandle());
+ }
+ }
+ }
+
+ @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+ public void testResNApiNXDomainPrivateDns() throws InterruptedException {
+ mCtsNetUtils.storePrivateDnsSetting();
+ // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
+ // b/144521720
+ try {
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ for (Network network : getTestableNetworks()) {
+ // Wait for private DNS setting to propagate.
+ mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
+ network, GOOGLE_PRIVATE_DNS_SERVER, true);
+ runResNnxDomainCheck(network.getNetworkHandle());
+ }
+ } finally {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
new file mode 100644
index 0000000..1c9aba1
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -0,0 +1,867 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.app.Instrumentation
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.INetworkAgent
+import android.net.INetworkAgentRegistry
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.KeepalivePacketData
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NattKeepalivePacketData
+import android.net.Network
+import android.net.NetworkAgent
+import android.net.NetworkAgent.INVALID_NETWORK
+import android.net.NetworkAgent.VALID_NETWORK
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkInfo
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.RouteInfo
+import android.net.SocketKeepalive
+import android.net.Uri
+import android.net.VpnManager
+import android.net.VpnTransportInfo
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSignalStrengthThresholdsUpdated
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStartSocketKeepalive
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import android.os.Build
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.os.SystemClock
+import android.util.DebugUtils.valueToString
+import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.ThrowingSupplier
+import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Losing
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import java.time.Duration
+import java.util.Arrays
+import java.util.UUID
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+// This test doesn't really have a constraint on how fast the methods should return. If it's
+// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
+// without affecting the run time of successful runs. Thus, set a very high timeout.
+private const val DEFAULT_TIMEOUT_MS = 5000L
+// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
+// only possible thing (the relevant handler is the one in the real ConnectivityService,
+// and then there is the Binder call), so have a short timeout for this as it will be
+// exhausted every time.
+private const val NO_CALLBACK_TIMEOUT = 200L
+// Any legal score (0~99) for the test network would do, as it is going to be kept up by the
+// requests filed by the test and should never match normal internet requests. 70 is the default
+// score of Ethernet networks, it's as good a value as any other.
+private const val TEST_NETWORK_SCORE = 70
+private const val WORSE_NETWORK_SCORE = 65
+private const val BETTER_NETWORK_SCORE = 75
+private const val FAKE_NET_ID = 1098
+private val instrumentation: Instrumentation
+ get() = InstrumentationRegistry.getInstrumentation()
+private val realContext: Context
+ get() = InstrumentationRegistry.getContext()
+private fun Message(what: Int, arg1: Int, arg2: Int, obj: Any?) = Message.obtain().also {
+ it.what = what
+ it.arg1 = arg1
+ it.arg2 = arg2
+ it.obj = obj
+}
+
+@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)
+class NetworkAgentTest {
+ private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
+ private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
+
+ private val mCM = realContext.getSystemService(ConnectivityManager::class.java)
+ private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+ private val mFakeConnectivityService = FakeConnectivityService()
+
+ private class Provider(context: Context, looper: Looper) :
+ NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
+
+ private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+ private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+ @Before
+ fun setUp() {
+ instrumentation.getUiAutomation().adoptShellPermissionIdentity()
+ mHandlerThread.start()
+ }
+
+ @After
+ fun tearDown() {
+ agentsToCleanUp.forEach { it.unregister() }
+ callbacksToCleanUp.forEach { mCM.unregisterNetworkCallback(it) }
+ mHandlerThread.quitSafely()
+ instrumentation.getUiAutomation().dropShellPermissionIdentity()
+ }
+
+ /**
+ * A fake that helps simulating ConnectivityService talking to a harnessed agent.
+ * This fake only supports speaking to one harnessed agent at a time because it
+ * only keeps track of one async channel.
+ */
+ private class FakeConnectivityService {
+ val mockRegistry = mock(INetworkAgentRegistry::class.java)
+ private var agentField: INetworkAgent? = null
+ private val registry = object : INetworkAgentRegistry.Stub(),
+ INetworkAgentRegistry by mockRegistry {
+ // asBinder has implementations in both INetworkAgentRegistry.Stub and mockRegistry, so
+ // it needs to be disambiguated. Just fail the test as it should be unused here.
+ // asBinder is used when sending the registry in binder transactions, so not in this
+ // test (the test just uses in-process direct calls). If it were used across processes,
+ // using the Stub super.asBinder() implementation would allow sending the registry in
+ // binder transactions, while recording incoming calls on the other mockito-generated
+ // methods.
+ override fun asBinder() = fail("asBinder should be unused in this test")
+ }
+
+ val agent: INetworkAgent
+ get() = agentField ?: fail("No INetworkAgent")
+
+ fun connect(agent: INetworkAgent) {
+ this.agentField = agent
+ agent.onRegistered(registry)
+ }
+
+ fun disconnect() = agent.onDisconnected()
+ }
+
+ private open class TestableNetworkAgent(
+ context: Context,
+ looper: Looper,
+ val nc: NetworkCapabilities,
+ val lp: LinkProperties,
+ conf: NetworkAgentConfig
+ ) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
+ nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ private val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+ sealed class CallbackEntry {
+ object OnBandwidthUpdateRequested : CallbackEntry()
+ object OnNetworkUnwanted : CallbackEntry()
+ data class OnAddKeepalivePacketFilter(
+ val slot: Int,
+ val packet: KeepalivePacketData
+ ) : CallbackEntry()
+ data class OnRemoveKeepalivePacketFilter(val slot: Int) : CallbackEntry()
+ data class OnStartSocketKeepalive(
+ val slot: Int,
+ val interval: Int,
+ val packet: KeepalivePacketData
+ ) : CallbackEntry()
+ data class OnStopSocketKeepalive(val slot: Int) : CallbackEntry()
+ data class OnSaveAcceptUnvalidated(val accept: Boolean) : CallbackEntry()
+ object OnAutomaticReconnectDisabled : CallbackEntry()
+ data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry()
+ data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
+ object OnNetworkCreated : CallbackEntry()
+ object OnNetworkDestroyed : CallbackEntry()
+ }
+
+ override fun onBandwidthUpdateRequested() {
+ history.add(OnBandwidthUpdateRequested)
+ }
+
+ override fun onNetworkUnwanted() {
+ history.add(OnNetworkUnwanted)
+ }
+
+ override fun onAddKeepalivePacketFilter(slot: Int, packet: KeepalivePacketData) {
+ history.add(OnAddKeepalivePacketFilter(slot, packet))
+ }
+
+ override fun onRemoveKeepalivePacketFilter(slot: Int) {
+ history.add(OnRemoveKeepalivePacketFilter(slot))
+ }
+
+ override fun onStartSocketKeepalive(
+ slot: Int,
+ interval: Duration,
+ packet: KeepalivePacketData
+ ) {
+ history.add(OnStartSocketKeepalive(slot, interval.seconds.toInt(), packet))
+ }
+
+ override fun onStopSocketKeepalive(slot: Int) {
+ history.add(OnStopSocketKeepalive(slot))
+ }
+
+ override fun onSaveAcceptUnvalidated(accept: Boolean) {
+ history.add(OnSaveAcceptUnvalidated(accept))
+ }
+
+ override fun onAutomaticReconnectDisabled() {
+ history.add(OnAutomaticReconnectDisabled)
+ }
+
+ override fun onSignalStrengthThresholdsUpdated(thresholds: IntArray) {
+ history.add(OnSignalStrengthThresholdsUpdated(thresholds))
+ }
+
+ fun expectEmptySignalStrengths() {
+ expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+ // intArrayOf() without arguments makes an empty array
+ assertArrayEquals(intArrayOf(), it.thresholds)
+ }
+ }
+
+ override fun onValidationStatus(status: Int, uri: Uri?) {
+ history.add(OnValidationStatus(status, uri))
+ }
+
+ override fun onNetworkCreated() {
+ history.add(OnNetworkCreated)
+ }
+
+ override fun onNetworkDestroyed() {
+ history.add(OnNetworkDestroyed)
+ }
+
+ // Expects the initial validation event that always occurs immediately after registering
+ // a NetworkAgent whose network does not require validation (which test networks do
+ // not, since they lack the INTERNET capability). It always contains the default argument
+ // for the URI.
+ fun expectNoInternetValidationStatus() = expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, VALID_NETWORK)
+ // The returned Uri is parsed from the empty string, which means it's an
+ // instance of the (private) Uri.StringUri. There are no real good ways
+ // to check this, the least bad is to just convert it to a string and
+ // make sure it's empty.
+ assertEquals("", it.uri.toString())
+ }
+
+ inline fun <reified T : CallbackEntry> expectCallback(): T {
+ val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+ assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+ return foundCallback
+ }
+
+ fun assertNoCallback() {
+ assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS),
+ "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
+ assertNull(history.peek())
+ }
+ }
+
+ private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) {
+ mCM.requestNetwork(request, callback)
+ callbacksToCleanUp.add(callback)
+ }
+
+ private fun registerNetworkCallback(
+ request: NetworkRequest,
+ callback: TestableNetworkCallback
+ ) {
+ mCM.registerNetworkCallback(request, callback)
+ callbacksToCleanUp.add(callback)
+ }
+
+ private fun createNetworkAgent(
+ context: Context = realContext,
+ name: String? = null,
+ initialNc: NetworkCapabilities? = null,
+ initialLp: LinkProperties? = null,
+ initialConfig: NetworkAgentConfig? = null
+ ): TestableNetworkAgent {
+ val nc = initialNc ?: NetworkCapabilities().apply {
+ addTransportType(TRANSPORT_TEST)
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ removeCapability(NET_CAPABILITY_INTERNET)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VPN)
+ if (SdkLevel.isAtLeastS()) {
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ }
+ if (null != name) {
+ setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name))
+ }
+ }
+ val lp = initialLp ?: LinkProperties().apply {
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+ }
+ val config = initialConfig ?: NetworkAgentConfig.Builder().build()
+ return TestableNetworkAgent(context, mHandlerThread.looper, nc, lp, config).also {
+ agentsToCleanUp.add(it)
+ }
+ }
+
+ private fun createConnectedNetworkAgent(context: Context = realContext, name: String? = null):
+ Pair<TestableNetworkAgent, TestableNetworkCallback> {
+ val request: NetworkRequest = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+ val agent = createNetworkAgent(context, name)
+ agent.setTeardownDelayMillis(0)
+ agent.register()
+ agent.markConnected()
+ agent.expectCallback<OnNetworkCreated>()
+ return agent to callback
+ }
+
+ private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also {
+ mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
+ }
+
+ @Test
+ fun testConnectAndUnregister() {
+ val (agent, callback) = createConnectedNetworkAgent()
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectEmptySignalStrengths()
+ agent.expectNoInternetValidationStatus()
+ agent.unregister()
+ callback.expectCallback<Lost>(agent.network)
+ agent.expectCallback<OnNetworkUnwanted>()
+ assertFailsWith<IllegalStateException>("Must not be able to register an agent twice") {
+ agent.register()
+ }
+ agent.expectCallback<OnNetworkDestroyed>()
+ }
+
+ @Test
+ fun testOnBandwidthUpdateRequested() {
+ val (agent, callback) = createConnectedNetworkAgent()
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectEmptySignalStrengths()
+ agent.expectNoInternetValidationStatus()
+ mCM.requestBandwidthUpdate(agent.network)
+ agent.expectCallback<OnBandwidthUpdateRequested>()
+ agent.unregister()
+ }
+
+ @Test
+ fun testSignalStrengthThresholds() {
+ val thresholds = intArrayOf(30, 50, 65)
+ val callbacks = thresholds.map { strength ->
+ val request = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setSignalStrength(strength)
+ .build()
+ TestableNetworkCallback(DEFAULT_TIMEOUT_MS).also {
+ registerNetworkCallback(request, it)
+ }
+ }
+ createConnectedNetworkAgent().let { (agent, callback) ->
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+ assertArrayEquals(it.thresholds, thresholds)
+ }
+ agent.expectNoInternetValidationStatus()
+
+ // Send signal strength and check that the callbacks are called appropriately.
+ val nc = NetworkCapabilities(agent.nc)
+ nc.setSignalStrength(20)
+ agent.sendNetworkCapabilities(nc)
+ callbacks.forEach { it.assertNoCallback(NO_CALLBACK_TIMEOUT) }
+
+ nc.setSignalStrength(40)
+ agent.sendNetworkCapabilities(nc)
+ callbacks[0].expectAvailableCallbacks(agent.network)
+ callbacks[1].assertNoCallback(NO_CALLBACK_TIMEOUT)
+ callbacks[2].assertNoCallback(NO_CALLBACK_TIMEOUT)
+
+ nc.setSignalStrength(80)
+ agent.sendNetworkCapabilities(nc)
+ callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 80 }
+ callbacks[1].expectAvailableCallbacks(agent.network)
+ callbacks[2].expectAvailableCallbacks(agent.network)
+
+ nc.setSignalStrength(55)
+ agent.sendNetworkCapabilities(nc)
+ callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 }
+ callbacks[1].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 }
+ callbacks[2].expectCallback<Lost>(agent.network)
+ }
+ callbacks.forEach {
+ mCM.unregisterNetworkCallback(it)
+ }
+ }
+
+ @Test
+ fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent ->
+ val packet = NattKeepalivePacketData(
+ LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */,
+ REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */,
+ ByteArray(100 /* size */))
+ val slot = 4
+ val interval = 37
+
+ mFakeConnectivityService.agent.onAddNattKeepalivePacketFilter(slot, packet)
+ mFakeConnectivityService.agent.onStartNattSocketKeepalive(slot, interval, packet)
+
+ agent.expectCallback<OnAddKeepalivePacketFilter>().let {
+ assertEquals(it.slot, slot)
+ assertEquals(it.packet, packet)
+ }
+ agent.expectCallback<OnStartSocketKeepalive>().let {
+ assertEquals(it.slot, slot)
+ assertEquals(it.interval, interval)
+ assertEquals(it.packet, packet)
+ }
+
+ agent.assertNoCallback()
+
+ // Check that when the agent sends a keepalive event, ConnectivityService receives the
+ // expected message.
+ agent.sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED)
+ verify(mFakeConnectivityService.mockRegistry, timeout(DEFAULT_TIMEOUT_MS))
+ .sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED)
+
+ mFakeConnectivityService.agent.onStopSocketKeepalive(slot)
+ mFakeConnectivityService.agent.onRemoveKeepalivePacketFilter(slot)
+ agent.expectCallback<OnStopSocketKeepalive>().let {
+ assertEquals(it.slot, slot)
+ }
+ agent.expectCallback<OnRemoveKeepalivePacketFilter>().let {
+ assertEquals(it.slot, slot)
+ }
+ }
+
+ @Test
+ fun testSendUpdates(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectEmptySignalStrengths()
+ agent.expectNoInternetValidationStatus()
+ val ifaceName = "adhocIface"
+ val lp = LinkProperties(agent.lp)
+ lp.setInterfaceName(ifaceName)
+ agent.sendLinkProperties(lp)
+ callback.expectLinkPropertiesThat(agent.network) {
+ it.getInterfaceName() == ifaceName
+ }
+ val nc = NetworkCapabilities(agent.nc)
+ nc.addCapability(NET_CAPABILITY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc)
+ callback.expectCapabilitiesThat(agent.network) {
+ it.hasCapability(NET_CAPABILITY_NOT_METERED)
+ }
+ }
+
+ @Test
+ fun testSendScore() {
+ // This test will create two networks and check that the one with the stronger
+ // score wins out for a request that matches them both.
+ // First create requests to make sure both networks are kept up, using the
+ // specifier so they are specific to each network
+ val name1 = UUID.randomUUID().toString()
+ val name2 = UUID.randomUUID().toString()
+ val request1 = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
+ .build()
+ val request2 = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name2))
+ .build()
+ val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ val callback2 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request1, callback1)
+ requestNetwork(request2, callback2)
+
+ // Then file the interesting request
+ val request = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+
+ // Connect the first Network
+ createConnectedNetworkAgent(name = name1).let { (agent1, _) ->
+ callback.expectAvailableThenValidatedCallbacks(agent1.network)
+ // If using the int ranking, agent1 must be upgraded to a better score so that there is
+ // no ambiguity when agent2 connects that agent1 is still better. If using policy
+ // ranking, this is not necessary.
+ agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+ .build())
+ // Connect the second agent
+ createConnectedNetworkAgent(name = name2).let { (agent2, _) ->
+ agent2.markConnected()
+ // The callback should not see anything yet. With int ranking, agent1 was upgraded
+ // to a stronger score beforehand. With policy ranking, agent1 is preferred by
+ // virtue of already satisfying the request.
+ callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ // Now downgrade the score and expect the callback now prefers agent2
+ agent1.sendNetworkScore(NetworkScore.Builder()
+ .setLegacyInt(WORSE_NETWORK_SCORE)
+ .setExiting(true)
+ .build())
+ callback.expectCallback<Available>(agent2.network)
+ }
+ }
+
+ // tearDown() will unregister the requests and agents
+ }
+
+ private fun hasAllTransports(nc: NetworkCapabilities?, transports: IntArray) =
+ nc != null && transports.all { nc.hasTransport(it) }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ fun testSetUnderlyingNetworksAndVpnSpecifier() {
+ val mySessionId = "MySession12345"
+ val request = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_VPN)
+ .removeCapability(NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NET_CAPABILITY_TRUSTED) // TODO: add to VPN!
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ registerNetworkCallback(request, callback)
+
+ val nc = NetworkCapabilities().apply {
+ addTransportType(TRANSPORT_TEST)
+ addTransportType(TRANSPORT_VPN)
+ removeCapability(NET_CAPABILITY_NOT_VPN)
+ setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, mySessionId))
+ if (SdkLevel.isAtLeastS()) {
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ }
+ }
+ val defaultNetwork = mCM.activeNetwork
+ assertNotNull(defaultNetwork)
+ val defaultNetworkCapabilities = mCM.getNetworkCapabilities(defaultNetwork)
+ val defaultNetworkTransports = defaultNetworkCapabilities.transportTypes
+
+ val agent = createNetworkAgent(initialNc = nc)
+ agent.register()
+ agent.markConnected()
+ callback.expectAvailableThenValidatedCallbacks(agent.network!!)
+
+ // Check that the default network's transport is propagated to the VPN.
+ var vpnNc = mCM.getNetworkCapabilities(agent.network)
+ assertNotNull(vpnNc)
+ assertEquals(VpnManager.TYPE_VPN_SERVICE,
+ (vpnNc.transportInfo as VpnTransportInfo).type)
+ assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
+
+ val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
+ assertTrue(hasAllTransports(vpnNc, testAndVpn))
+ assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_VPN))
+ assertTrue(hasAllTransports(vpnNc, defaultNetworkTransports),
+ "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
+ " lacking transports from ${Arrays.toString(defaultNetworkTransports)}")
+
+ // Check that when no underlying networks are announced the underlying transport disappears.
+ agent.setUnderlyingNetworks(listOf<Network>())
+ callback.expectCapabilitiesThat(agent.network!!) {
+ it.transportTypes.size == 2 && hasAllTransports(it, testAndVpn)
+ }
+
+ // Put the underlying network back and check that the underlying transport reappears.
+ val expectedTransports = (defaultNetworkTransports.toSet() + TRANSPORT_TEST + TRANSPORT_VPN)
+ .toIntArray()
+ agent.setUnderlyingNetworks(null)
+ callback.expectCapabilitiesThat(agent.network!!) {
+ it.transportTypes.size == expectedTransports.size &&
+ hasAllTransports(it, expectedTransports)
+ }
+
+ // Check that some underlying capabilities are propagated.
+ // This is not very accurate because the test does not control the capabilities of the
+ // underlying networks, and because not congested, not roaming, and not suspended are the
+ // default anyway. It's still useful as an extra check though.
+ vpnNc = mCM.getNetworkCapabilities(agent.network)
+ for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
+ NET_CAPABILITY_NOT_ROAMING,
+ NET_CAPABILITY_NOT_SUSPENDED)) {
+ val capStr = valueToString(NetworkCapabilities::class.java, "NET_CAPABILITY_", cap)
+ if (defaultNetworkCapabilities.hasCapability(cap) && !vpnNc.hasCapability(cap)) {
+ fail("$capStr not propagated from underlying: $defaultNetworkCapabilities")
+ }
+ }
+
+ agent.unregister()
+ callback.expectCallback<Lost>(agent.network)
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ fun testAgentStartsInConnecting() {
+ val mockContext = mock(Context::class.java)
+ val mockCm = mock(ConnectivityManager::class.java)
+ doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
+ val agent = createNetworkAgent(mockContext)
+ agent.register()
+ verify(mockCm).registerNetworkAgent(any(),
+ argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
+ any(LinkProperties::class.java),
+ any(NetworkCapabilities::class.java),
+ any(NetworkScore::class.java),
+ any(NetworkAgentConfig::class.java),
+ eq(NetworkProvider.ID_NONE))
+ }
+
+ @Test
+ fun testSetAcceptUnvalidated() {
+ createNetworkAgentWithFakeCS().let { agent ->
+ mFakeConnectivityService.agent.onSaveAcceptUnvalidated(true)
+ agent.expectCallback<OnSaveAcceptUnvalidated>().let {
+ assertTrue(it.accept)
+ }
+ agent.assertNoCallback()
+ }
+ }
+
+ @Test
+ fun testSetAcceptUnvalidatedPreventAutomaticReconnect() {
+ createNetworkAgentWithFakeCS().let { agent ->
+ mFakeConnectivityService.agent.onSaveAcceptUnvalidated(false)
+ mFakeConnectivityService.agent.onPreventAutomaticReconnect()
+ agent.expectCallback<OnSaveAcceptUnvalidated>().let {
+ assertFalse(it.accept)
+ }
+ agent.expectCallback<OnAutomaticReconnectDisabled>()
+ agent.assertNoCallback()
+ // When automatic reconnect is turned off, the network is torn down and
+ // ConnectivityService disconnects. As part of the disconnect, ConnectivityService will
+ // also send itself a message to unregister the NetworkAgent from its internal
+ // structure.
+ mFakeConnectivityService.disconnect()
+ agent.expectCallback<OnNetworkUnwanted>()
+ }
+ }
+
+ @Test
+ fun testPreventAutomaticReconnect() {
+ createNetworkAgentWithFakeCS().let { agent ->
+ mFakeConnectivityService.agent.onPreventAutomaticReconnect()
+ agent.expectCallback<OnAutomaticReconnectDisabled>()
+ agent.assertNoCallback()
+ mFakeConnectivityService.disconnect()
+ agent.expectCallback<OnNetworkUnwanted>()
+ }
+ }
+
+ @Test
+ fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent ->
+ val uri = Uri.parse("http://www.google.com")
+ mFakeConnectivityService.agent.onValidationStatusChanged(VALID_NETWORK,
+ uri.toString())
+ agent.expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, VALID_NETWORK)
+ assertEquals(it.uri, uri)
+ }
+
+ mFakeConnectivityService.agent.onValidationStatusChanged(INVALID_NETWORK, null)
+ agent.expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, INVALID_NETWORK)
+ assertNull(it.uri)
+ }
+ }
+
+ @Test
+ fun testTemporarilyUnmeteredCapability() {
+ // This test will create a networks with/without NET_CAPABILITY_TEMPORARILY_NOT_METERED
+ // and check that the callback reflects the capability changes.
+ // First create a request to make sure the network is kept up
+ val request1 = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .build()
+ val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS).also {
+ registerNetworkCallback(request1, it)
+ }
+ requestNetwork(request1, callback1)
+
+ // Then file the interesting request
+ val request = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+
+ // Connect the network
+ createConnectedNetworkAgent().let { (agent, _) ->
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+
+ // Send TEMP_NOT_METERED and check that the callback is called appropriately.
+ val nc1 = NetworkCapabilities(agent.nc)
+ .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc1)
+ callback.expectCapabilitiesThat(agent.network) {
+ it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+
+ // Remove TEMP_NOT_METERED and check that the callback is called appropriately.
+ val nc2 = NetworkCapabilities(agent.nc)
+ .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc2)
+ callback.expectCapabilitiesThat(agent.network) {
+ !it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+ }
+
+ // tearDown() will unregister the requests and agents
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ fun testSetLingerDuration() {
+ // This test will create two networks and check that the one with the stronger
+ // score wins out for a request that matches them both. And the weaker agent will
+ // be disconnected after customized linger duration.
+
+ // Connect the first Network
+ val name1 = UUID.randomUUID().toString()
+ val name2 = UUID.randomUUID().toString()
+ val (agent1, callback) = createConnectedNetworkAgent(name = name1)
+ callback.expectAvailableThenValidatedCallbacks(agent1.network!!)
+ // Downgrade agent1 to a worse score so that there is no ambiguity when
+ // agent2 connects.
+ agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(WORSE_NETWORK_SCORE)
+ .setExiting(true).build())
+
+ // Verify invalid linger duration cannot be set.
+ assertFailsWith<IllegalArgumentException> {
+ agent1.setLingerDuration(Duration.ofMillis(-1))
+ }
+ assertFailsWith<IllegalArgumentException> { agent1.setLingerDuration(Duration.ZERO) }
+ assertFailsWith<IllegalArgumentException> {
+ agent1.setLingerDuration(Duration.ofMillis(Integer.MIN_VALUE.toLong()))
+ }
+ assertFailsWith<IllegalArgumentException> {
+ agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong() + 1))
+ }
+ assertFailsWith<IllegalArgumentException> {
+ agent1.setLingerDuration(Duration.ofMillis(
+ NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+ }
+ // Verify valid linger timer can be set, but it should not take effect since the network
+ // is still needed.
+ agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong()))
+ callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ // Set to the value we want to verify the functionality.
+ agent1.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
+ // Make a listener which can observe agent1 lost later.
+ val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ registerNetworkCallback(NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
+ .build(), callbackWeaker)
+ callbackWeaker.expectAvailableCallbacks(agent1.network!!)
+
+ // Connect the second agent with a score better than agent1. Verify the callback for
+ // agent1 sees the linger expiry while the callback for both sees the winner.
+ // Record linger start timestamp prior to send score to prevent possible race, the actual
+ // timestamp should be slightly late than this since the service handles update
+ // network score asynchronously.
+ val lingerStart = SystemClock.elapsedRealtime()
+ val agent2 = createNetworkAgent(name = name2)
+ agent2.register()
+ agent2.markConnected()
+ callback.expectAvailableCallbacks(agent2.network!!)
+ callbackWeaker.expectCallback<Losing>(agent1.network!!)
+ val expectedRemainingLingerDuration = lingerStart +
+ NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
+ // If the available callback is too late. The remaining duration will be reduced.
+ assertTrue(expectedRemainingLingerDuration > 0,
+ "expected remaining linger duration is $expectedRemainingLingerDuration")
+ callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
+ callbackWeaker.expectCallback<Lost>(agent1.network!!)
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ fun testSetSubscriberId() {
+ val name = "TEST-AGENT"
+ val imsi = UUID.randomUUID().toString()
+ val config = NetworkAgentConfig.Builder().setSubscriberId(imsi).build()
+
+ val request: NetworkRequest = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name))
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+
+ val agent = createNetworkAgent(name = name, initialConfig = config)
+ agent.register()
+ agent.markConnected()
+ callback.expectAvailableThenValidatedCallbacks(agent.network!!)
+ val snapshots = runWithShellPermissionIdentity(ThrowingSupplier {
+ mCM!!.allNetworkStateSnapshots }, NETWORK_SETTINGS)
+ val testNetworkSnapshot = snapshots.findLast { it.network == agent.network }
+ assertEquals(imsi, testNetworkSnapshot!!.subscriberId)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
new file mode 100644
index 0000000..fa15e8f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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 android.net.cts
+
+import android.os.Build
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkInfo
+import android.net.NetworkInfo.DetailedState
+import android.net.NetworkInfo.State
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.junit.Test
+
+const val TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE
+const val TYPE_WIFI = ConnectivityManager.TYPE_WIFI
+const val MOBILE_TYPE_NAME = "mobile"
+const val WIFI_TYPE_NAME = "WIFI"
+const val LTE_SUBTYPE_NAME = "LTE"
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NetworkInfoTest {
+ @Rule @JvmField
+ val ignoreRule = DevSdkIgnoreRule()
+
+ @Test
+ fun testAccessNetworkInfoProperties() {
+ val cm = InstrumentationRegistry.getInstrumentation().context
+ .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val ni = cm.getAllNetworkInfo()
+ assertTrue(ni.isNotEmpty())
+
+ for (netInfo in ni) {
+ when (netInfo.getType()) {
+ TYPE_MOBILE -> assertNetworkInfo(netInfo, MOBILE_TYPE_NAME)
+ TYPE_WIFI -> assertNetworkInfo(netInfo, WIFI_TYPE_NAME)
+ // TODO: Add BLUETOOTH_TETHER testing
+ }
+ }
+ }
+
+ private fun assertNetworkInfo(netInfo: NetworkInfo, expectedTypeName: String) {
+ assertTrue(expectedTypeName.equals(netInfo.getTypeName(), ignoreCase = true))
+ assertNotNull(netInfo.toString())
+
+ if (!netInfo.isConnectedOrConnecting()) return
+
+ assertTrue(netInfo.isAvailable())
+ if (State.CONNECTED == netInfo.getState()) {
+ assertTrue(netInfo.isConnected())
+ }
+ assertTrue(State.CONNECTING == netInfo.getState() ||
+ State.CONNECTED == netInfo.getState())
+ assertTrue(DetailedState.SCANNING == netInfo.getDetailedState() ||
+ DetailedState.CONNECTING == netInfo.getDetailedState() ||
+ DetailedState.AUTHENTICATING == netInfo.getDetailedState() ||
+ DetailedState.CONNECTED == netInfo.getDetailedState())
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ fun testConstructor() {
+ val networkInfo = NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE,
+ MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
+
+ assertEquals(TYPE_MOBILE, networkInfo.type)
+ assertEquals(TelephonyManager.NETWORK_TYPE_LTE, networkInfo.subtype)
+ assertEquals(MOBILE_TYPE_NAME, networkInfo.typeName)
+ assertEquals(LTE_SUBTYPE_NAME, networkInfo.subtypeName)
+ assertEquals(DetailedState.IDLE, networkInfo.detailedState)
+ assertEquals(State.UNKNOWN, networkInfo.state)
+ assertNull(networkInfo.reason)
+ assertNull(networkInfo.extraInfo)
+
+ try {
+ NetworkInfo(ConnectivityManager.MAX_NETWORK_TYPE + 1,
+ TelephonyManager.NETWORK_TYPE_LTE, MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
+ fail("Unexpected behavior. Network type is invalid.")
+ } catch (e: IllegalArgumentException) {
+ // Expected behavior.
+ }
+ }
+
+ @Test
+ fun testSetDetailedState() {
+ val networkInfo = NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE,
+ MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
+ val reason = "TestNetworkInfo"
+ val extraReason = "setDetailedState test"
+
+ networkInfo.setDetailedState(DetailedState.CONNECTED, reason, extraReason)
+ assertEquals(DetailedState.CONNECTED, networkInfo.detailedState)
+ assertEquals(State.CONNECTED, networkInfo.state)
+ assertEquals(reason, networkInfo.reason)
+ assertEquals(extraReason, networkInfo.extraInfo)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java b/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java
new file mode 100644
index 0000000..590ce89
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+
+import android.net.NetworkInfo.DetailedState;
+import android.test.AndroidTestCase;
+
+public class NetworkInfo_DetailedStateTest extends AndroidTestCase {
+
+ public void testValueOf() {
+ assertEquals(DetailedState.AUTHENTICATING, DetailedState.valueOf("AUTHENTICATING"));
+ assertEquals(DetailedState.CONNECTED, DetailedState.valueOf("CONNECTED"));
+ assertEquals(DetailedState.CONNECTING, DetailedState.valueOf("CONNECTING"));
+ assertEquals(DetailedState.DISCONNECTED, DetailedState.valueOf("DISCONNECTED"));
+ assertEquals(DetailedState.DISCONNECTING, DetailedState.valueOf("DISCONNECTING"));
+ assertEquals(DetailedState.FAILED, DetailedState.valueOf("FAILED"));
+ assertEquals(DetailedState.IDLE, DetailedState.valueOf("IDLE"));
+ assertEquals(DetailedState.OBTAINING_IPADDR, DetailedState.valueOf("OBTAINING_IPADDR"));
+ assertEquals(DetailedState.SCANNING, DetailedState.valueOf("SCANNING"));
+ assertEquals(DetailedState.SUSPENDED, DetailedState.valueOf("SUSPENDED"));
+ }
+
+ public void testValues() {
+ DetailedState[] expected = DetailedState.values();
+ assertEquals(13, expected.length);
+ assertEquals(DetailedState.IDLE, expected[0]);
+ assertEquals(DetailedState.SCANNING, expected[1]);
+ assertEquals(DetailedState.CONNECTING, expected[2]);
+ assertEquals(DetailedState.AUTHENTICATING, expected[3]);
+ assertEquals(DetailedState.OBTAINING_IPADDR, expected[4]);
+ assertEquals(DetailedState.CONNECTED, expected[5]);
+ assertEquals(DetailedState.SUSPENDED, expected[6]);
+ assertEquals(DetailedState.DISCONNECTING, expected[7]);
+ assertEquals(DetailedState.DISCONNECTED, expected[8]);
+ assertEquals(DetailedState.FAILED, expected[9]);
+ assertEquals(DetailedState.BLOCKED, expected[10]);
+ assertEquals(DetailedState.VERIFYING_POOR_LINK, expected[11]);
+ assertEquals(DetailedState.CAPTIVE_PORTAL_CHECK, expected[12]);
+ }
+
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java b/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java
new file mode 100644
index 0000000..5303ef1
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.NetworkInfo.State;
+import android.test.AndroidTestCase;
+
+public class NetworkInfo_StateTest extends AndroidTestCase {
+
+ public void testValueOf() {
+ assertEquals(State.CONNECTED, State.valueOf("CONNECTED"));
+ assertEquals(State.CONNECTING, State.valueOf("CONNECTING"));
+ assertEquals(State.DISCONNECTED, State.valueOf("DISCONNECTED"));
+ assertEquals(State.DISCONNECTING, State.valueOf("DISCONNECTING"));
+ assertEquals(State.SUSPENDED, State.valueOf("SUSPENDED"));
+ assertEquals(State.UNKNOWN, State.valueOf("UNKNOWN"));
+ }
+
+ public void testValues() {
+ State[] expected = State.values();
+ assertEquals(6, expected.length);
+ assertEquals(State.CONNECTING, expected[0]);
+ assertEquals(State.CONNECTED, expected[1]);
+ assertEquals(State.SUSPENDED, expected[2]);
+ assertEquals(State.DISCONNECTING, expected[3]);
+ assertEquals(State.DISCONNECTED, expected[4]);
+ assertEquals(State.UNKNOWN, expected[5]);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
new file mode 100644
index 0000000..bca4456
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.net.MacAddress;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.wifi.WifiNetworkSpecifier;
+import android.os.Build;
+import android.os.PatternMatcher;
+import android.os.Process;
+import android.util.ArraySet;
+import android.util.Range;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.NetworkRequestShimImpl;
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final String TEST_SSID = "TestSSID";
+ private static final String OTHER_SSID = "OtherSSID";
+ private static final int TEST_UID = 2097;
+ private static final String TEST_PACKAGE_NAME = "test.package.name";
+ private static final MacAddress ARBITRARY_ADDRESS = MacAddress.fromString("3:5:8:12:9:2");
+
+ private class LocalNetworkSpecifier extends NetworkSpecifier {
+ private final int mId;
+
+ LocalNetworkSpecifier(int id) {
+ mId = id;
+ }
+
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ return other instanceof LocalNetworkSpecifier
+ && mId == ((LocalNetworkSpecifier) other).mId;
+ }
+ }
+
+ @Test
+ public void testCapabilities() {
+ assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
+ .hasCapability(NET_CAPABILITY_MMS));
+ assertFalse(new NetworkRequest.Builder().removeCapability(NET_CAPABILITY_MMS).build()
+ .hasCapability(NET_CAPABILITY_MMS));
+
+ final NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build();
+ // Verify request has no capabilities
+ verifyNoCapabilities(nr);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testTemporarilyNotMeteredCapability() {
+ assertTrue(new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
+ .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ assertFalse(new NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
+ .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ }
+
+ private void verifyNoCapabilities(NetworkRequest nr) {
+ // NetworkCapabilities.mNetworkCapabilities is defined as type long
+ final int MAX_POSSIBLE_CAPABILITY = Long.SIZE;
+ for(int bit = 0; bit < MAX_POSSIBLE_CAPABILITY; bit++) {
+ assertFalse(nr.hasCapability(bit));
+ }
+ }
+
+ @Test
+ public void testTransports() {
+ assertTrue(new NetworkRequest.Builder().addTransportType(TRANSPORT_BLUETOOTH).build()
+ .hasTransport(TRANSPORT_BLUETOOTH));
+ assertFalse(new NetworkRequest.Builder().removeTransportType(TRANSPORT_BLUETOOTH).build()
+ .hasTransport(TRANSPORT_BLUETOOTH));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testSpecifier() {
+ assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier());
+ final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
+ .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL))
+ .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
+ .build();
+ final NetworkSpecifier obtainedSpecifier = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .setNetworkSpecifier(specifier)
+ .build()
+ .getNetworkSpecifier();
+ assertEquals(obtainedSpecifier, specifier);
+
+ assertNull(new NetworkRequest.Builder()
+ .clearCapabilities()
+ .build()
+ .getNetworkSpecifier());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRequestorPackageName() {
+ assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
+ final String pkgName = "android.net.test";
+ final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .setRequestorPackageName(pkgName)
+ .build();
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .setCapabilities(nc)
+ .build();
+ assertEquals(pkgName, nr.getRequestorPackageName());
+ assertNull(new NetworkRequest.Builder()
+ .clearCapabilities()
+ .build()
+ .getRequestorPackageName());
+ }
+
+ private void addNotVcnManagedCapability(@NonNull NetworkCapabilities nc) {
+ if (SdkLevel.isAtLeastS()) {
+ nc.addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testCanBeSatisfiedBy() {
+ final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
+ final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
+
+ // Some requests are adding NOT_VCN_MANAGED capability automatically. Add it to the
+ // capabilities below for bypassing the check.
+ final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_MMS)
+ .addCapability(NET_CAPABILITY_INTERNET);
+ addNotVcnManagedCapability(capCellularMmsInternet);
+ final NetworkCapabilities capCellularVpnMmsInternet =
+ new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN);
+ addNotVcnManagedCapability(capCellularVpnMmsInternet);
+ final NetworkCapabilities capCellularMmsInternetSpecifier1 =
+ new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1);
+ addNotVcnManagedCapability(capCellularMmsInternetSpecifier1);
+ final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_VPN)
+ .setNetworkSpecifier(specifier1);
+ addNotVcnManagedCapability(capVpnInternetSpecifier1);
+ final NetworkCapabilities capCellularMmsInternetMatchallspecifier =
+ new NetworkCapabilities(capCellularMmsInternet)
+ .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+ addNotVcnManagedCapability(capCellularMmsInternetMatchallspecifier);
+ final NetworkCapabilities capCellularMmsInternetSpecifier2 =
+ new NetworkCapabilities(capCellularMmsInternet)
+ .setNetworkSpecifier(specifier2);
+ addNotVcnManagedCapability(capCellularMmsInternetSpecifier2);
+
+ final NetworkRequest requestCellularInternetSpecifier1 = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .setNetworkSpecifier(specifier1)
+ .build();
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(null));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(new NetworkCapabilities()));
+ assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+ capCellularMmsInternetMatchallspecifier));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularMmsInternet));
+ assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+ capCellularMmsInternetSpecifier1));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularVpnMmsInternet));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+ capCellularMmsInternetSpecifier2));
+
+ final NetworkRequest requestCellularInternet = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternet));
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier1));
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier2));
+ assertFalse(requestCellularInternet.canBeSatisfiedBy(capVpnInternetSpecifier1));
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularVpnMmsInternet));
+ }
+
+ private void setUids(NetworkRequest.Builder builder, Set<Range<Integer>> ranges)
+ throws UnsupportedApiLevelException {
+ if (SdkLevel.isAtLeastS()) {
+ final NetworkRequestShim networkRequestShim = NetworkRequestShimImpl.newInstance();
+ networkRequestShim.setUids(builder, ranges);
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testInvariantInCanBeSatisfiedBy() {
+ // Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
+ // NetworkCapabilities.satisfiedByNetworkCapabilities().
+ final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
+ final int uid = Process.myUid();
+ final NetworkRequest.Builder nrBuilder = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .setLinkUpstreamBandwidthKbps(1000)
+ .setNetworkSpecifier(specifier1)
+ .setSignalStrength(-123);
+
+ // The uid ranges should be set into the request, but setUids() takes a set of UidRange
+ // that is hidden and inaccessible from shims. Before, S setUids will be a no-op. But
+ // because NetworkRequest.Builder sets the UID of the request to the current UID, the
+ // request contains the current UID both on S and before S.
+ final Set<Range<Integer>> ranges = new ArraySet<>();
+ ranges.add(new Range<Integer>(uid, uid));
+ try {
+ setUids(nrBuilder, ranges);
+ } catch (UnsupportedApiLevelException e) {
+ // Not supported before API31.
+ }
+ final NetworkRequest requestCombination = nrBuilder.build();
+
+ final NetworkCapabilities capCell = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ assertCorrectlySatisfies(false, requestCombination, capCell);
+
+ final NetworkCapabilities capCellInternet = new NetworkCapabilities.Builder(capCell)
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ assertCorrectlySatisfies(false, requestCombination, capCellInternet);
+
+ final NetworkCapabilities capCellInternetBW =
+ new NetworkCapabilities.Builder(capCellInternet)
+ .setLinkUpstreamBandwidthKbps(1024).build();
+ assertCorrectlySatisfies(false, requestCombination, capCellInternetBW);
+
+ final NetworkCapabilities capCellInternetBWSpecifier1 =
+ new NetworkCapabilities.Builder(capCellInternetBW)
+ .setNetworkSpecifier(specifier1).build();
+ assertCorrectlySatisfies(false, requestCombination, capCellInternetBWSpecifier1);
+
+ final NetworkCapabilities capCellInternetBWSpecifier1Signal =
+ new NetworkCapabilities.Builder(capCellInternetBWSpecifier1)
+ .setSignalStrength(-123).build();
+ addNotVcnManagedCapability(capCellInternetBWSpecifier1Signal);
+ assertCorrectlySatisfies(true, requestCombination,
+ capCellInternetBWSpecifier1Signal);
+
+ final NetworkCapabilities capCellInternetBWSpecifier1SignalUid =
+ new NetworkCapabilities.Builder(capCellInternetBWSpecifier1Signal)
+ .setOwnerUid(uid)
+ .setAdministratorUids(new int [] {uid}).build();
+ assertCorrectlySatisfies(true, requestCombination,
+ capCellInternetBWSpecifier1SignalUid);
+ }
+
+ private void assertCorrectlySatisfies(boolean expect, NetworkRequest request,
+ NetworkCapabilities nc) {
+ assertEquals(expect, request.canBeSatisfiedBy(nc));
+ assertEquals(
+ request.canBeSatisfiedBy(nc),
+ request.networkCapabilities.satisfiedByNetworkCapabilities(nc));
+ }
+
+ private static Set<Range<Integer>> uidRangesForUid(int uid) {
+ final Range<Integer> range = new Range<>(uid, uid);
+ return Set.of(range);
+ }
+
+ @Test
+ public void testSetIncludeOtherUidNetworks() throws Exception {
+ assumeTrue(TestUtils.shouldTestSApis());
+ final NetworkRequestShim shim = NetworkRequestShimImpl.newInstance();
+
+ final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ // NetworkRequests have NET_CAPABILITY_NOT_VCN_MANAGED by default.
+ builder.removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+ shim.setIncludeOtherUidNetworks(builder, false);
+ final NetworkRequest request = builder.build();
+
+ final NetworkRequest.Builder otherUidsBuilder = new NetworkRequest.Builder();
+ otherUidsBuilder.removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+ shim.setIncludeOtherUidNetworks(otherUidsBuilder, true);
+ final NetworkRequest otherUidsRequest = otherUidsBuilder.build();
+
+ assertNotEquals(Process.SYSTEM_UID, Process.myUid());
+ final NetworkCapabilities ncWithMyUid = new NetworkCapabilities()
+ .setUids(uidRangesForUid(Process.myUid()));
+ final NetworkCapabilities ncWithOtherUid = new NetworkCapabilities()
+ .setUids(uidRangesForUid(Process.SYSTEM_UID));
+
+ assertTrue(request + " should be satisfied by " + ncWithMyUid,
+ request.canBeSatisfiedBy(ncWithMyUid));
+ assertTrue(otherUidsRequest + " should be satisfied by " + ncWithMyUid,
+ otherUidsRequest.canBeSatisfiedBy(ncWithMyUid));
+ assertFalse(request + " should not be satisfied by " + ncWithOtherUid,
+ request.canBeSatisfiedBy(ncWithOtherUid));
+ assertTrue(otherUidsRequest + " should be satisfied by " + ncWithOtherUid,
+ otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid));
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRequestorUid() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ // Verify default value is INVALID_UID
+ assertEquals(Process.INVALID_UID, new NetworkRequest.Builder()
+ .setCapabilities(nc).build().getRequestorUid());
+
+ nc.setRequestorUid(1314);
+ final NetworkRequest nr = new NetworkRequest.Builder().setCapabilities(nc).build();
+ assertEquals(1314, nr.getRequestorUid());
+
+ assertEquals(Process.INVALID_UID, new NetworkRequest.Builder()
+ .clearCapabilities().build().getRequestorUid());
+ }
+
+ // TODO: 1. Refactor test cases with helper method.
+ // 2. Test capability that does not yet exist.
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBypassingVcn() {
+ // Make an empty request. Verify the NOT_VCN_MANAGED is added.
+ final NetworkRequest emptyRequest = new NetworkRequest.Builder().build();
+ assertTrue(emptyRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a request explicitly add NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is preserved.
+ final NetworkRequest mmsAddNotVcnRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_MMS)
+ .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ assertTrue(mmsAddNotVcnRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Similar to above, but the opposite order.
+ final NetworkRequest mmsAddNotVcnRequest2 = new NetworkRequest.Builder()
+ .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_MMS)
+ .build();
+ assertTrue(mmsAddNotVcnRequest2.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a request explicitly remove NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is removed.
+ final NetworkRequest removeNotVcnRequest = new NetworkRequest.Builder()
+ .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+ assertFalse(removeNotVcnRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a request add some capability inside VCN supported capabilities.
+ // Verify the NOT_VCN_MANAGED is added.
+ final NetworkRequest notRoamRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_NOT_ROAMING).build();
+ assertTrue(notRoamRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make an internet request. Verify the NOT_VCN_MANAGED is added.
+ final NetworkRequest internetRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ assertTrue(internetRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make an internet request which explicitly removed NOT_VCN_MANAGED.
+ // Verify the NOT_VCN_MANAGED is removed.
+ final NetworkRequest internetRemoveNotVcnRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+ assertFalse(internetRemoveNotVcnRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a normal MMS request. Verify the request could bypass VCN.
+ final NetworkRequest mmsRequest =
+ new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build();
+ assertFalse(mmsRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a SUPL request along with internet. Verify NOT_VCN_MANAGED is not added since
+ // SUPL is not in the supported list.
+ final NetworkRequest suplWithInternetRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_SUPL)
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ assertFalse(suplWithInternetRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a FOTA request with explicitly add NOT_VCN_MANAGED capability. Verify
+ // NOT_VCN_MANAGED is preserved.
+ final NetworkRequest fotaRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_FOTA)
+ .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+ assertTrue(fotaRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a DUN request, which is in {@code VCN_SUPPORTED_CAPABILITIES}.
+ // Verify NOT_VCN_MANAGED is preserved.
+ final NetworkRequest dunRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_DUN).build();
+ assertTrue(dunRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make an internet request but with NetworkSpecifier. Verify the NOT_VCN_MANAGED is not
+ // added.
+ final NetworkRequest internetWithSpecifierRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET)
+ .setNetworkSpecifier(makeTestWifiSpecifier()).build();
+ assertFalse(internetWithSpecifierRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+ }
+
+ private void verifyEqualRequestBuilt(NetworkRequest orig) {
+ try {
+ final NetworkRequestShim shim = NetworkRequestShimImpl.newInstance();
+ final NetworkRequest copy = shim.newBuilder(orig).build();
+ assertEquals(orig, copy);
+ } catch (UnsupportedApiLevelException e) {
+ fail("NetworkRequestShim.newBuilder should be supported in this SDK version");
+ }
+ }
+
+ @Test
+ public void testBuildRequestFromExistingRequestWithBuilder() {
+ assumeTrue(TestUtils.shouldTestSApis());
+ final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+
+ final NetworkRequest baseRequest = builder.build();
+ verifyEqualRequestBuilt(baseRequest);
+
+ final NetworkRequest requestCellMms = builder
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_MMS)
+ .setSignalStrength(-99).build();
+ verifyEqualRequestBuilt(requestCellMms);
+
+ final NetworkRequest requestWifi = builder
+ .addTransportType(TRANSPORT_WIFI)
+ .removeTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .removeCapability(NET_CAPABILITY_MMS)
+ .setNetworkSpecifier(makeTestWifiSpecifier())
+ .setSignalStrength(-33).build();
+ verifyEqualRequestBuilt(requestWifi);
+ }
+
+ private WifiNetworkSpecifier makeTestWifiSpecifier() {
+ return new WifiNetworkSpecifier.Builder()
+ .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL))
+ .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
+ .build();
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt b/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt
new file mode 100644
index 0000000..1a7f955
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 android.net.cts
+
+import android.content.pm.PackageManager
+import android.net.cts.util.CtsNetUtils
+import android.net.wifi.WifiManager
+import android.os.Build
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+/**
+ * Basic tests for APIs used by the network stack module.
+ */
+class NetworkStackDependenciesTest {
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+ fun testGetFrequency() {
+ // WifiInfo#getFrequency was missing a CTS test in Q: this test is run as part of MTS on Q
+ // devices to ensure it behaves correctly.
+ val context = InstrumentationRegistry.getInstrumentation().getContext()
+ assumeTrue("This test only applies to devices that support wifi",
+ context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI))
+ val wifiManager = context.getSystemService(WifiManager::class.java)
+ assertNotNull(wifiManager, "Device supports wifi but there is no WifiManager")
+
+ CtsNetUtils(context).ensureWifiConnected()
+ val wifiInfo = wifiManager.getConnectionInfo()
+ // The NetworkStack can handle any value of getFrequency; unknown frequencies will not be
+ // classified in metrics, but this is expected behavior. It is only important that the
+ // method does not crash. Still verify that the frequency is positive
+ val frequency = wifiInfo.getFrequency()
+ assertTrue(frequency > 0, "Frequency must be > 0")
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
new file mode 100644
index 0000000..1a48983
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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 android.net.cts;
+
+import static android.os.Process.INVALID_UID;
+
+import static org.junit.Assert.assertEquals;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.INetworkStatsService;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.AndroidTestCase;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsBinderTest {
+ // NOTE: These are shamelessly copied from TrafficStats.
+ private static final int TYPE_RX_BYTES = 0;
+ private static final int TYPE_RX_PACKETS = 1;
+ private static final int TYPE_TX_BYTES = 2;
+ private static final int TYPE_TX_PACKETS = 3;
+
+ @Rule
+ public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
+ Build.VERSION_CODES.Q /* ignoreClassUpTo */);
+
+ private final SparseArray<Function<Integer, Long>> mUidStatsQueryOpArray = new SparseArray<>();
+
+ @Before
+ public void setUp() throws Exception {
+ mUidStatsQueryOpArray.put(TYPE_RX_BYTES, uid -> TrafficStats.getUidRxBytes(uid));
+ mUidStatsQueryOpArray.put(TYPE_RX_PACKETS, uid -> TrafficStats.getUidRxPackets(uid));
+ mUidStatsQueryOpArray.put(TYPE_TX_BYTES, uid -> TrafficStats.getUidTxBytes(uid));
+ mUidStatsQueryOpArray.put(TYPE_TX_PACKETS, uid -> TrafficStats.getUidTxPackets(uid));
+ }
+
+ private long getUidStatsFromBinder(int uid, int type) throws Exception {
+ Method getServiceMethod = Class.forName("android.os.ServiceManager")
+ .getDeclaredMethod("getService", new Class[]{String.class});
+ IBinder binder = (IBinder) getServiceMethod.invoke(null, Context.NETWORK_STATS_SERVICE);
+ INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder);
+ return nss.getUidStats(uid, type);
+ }
+
+ private int getFirstAppUidThat(@NonNull Predicate<Integer> predicate) {
+ PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
+ List<PackageInfo> apps = pm.getInstalledPackages(0 /* flags */);
+ final PackageInfo match = CollectionUtils.find(apps,
+ it -> it.applicationInfo != null && predicate.test(it.applicationInfo.uid));
+ if (match != null) return match.applicationInfo.uid;
+ return INVALID_UID;
+ }
+
+ @Test
+ public void testAccessUidStatsFromBinder() throws Exception {
+ final int myUid = Process.myUid();
+ final List<Integer> testUidList = new ArrayList<>();
+
+ // Prepare uid list for testing.
+ testUidList.add(INVALID_UID);
+ testUidList.add(Process.ROOT_UID);
+ testUidList.add(Process.SYSTEM_UID);
+ testUidList.add(myUid);
+ testUidList.add(Process.LAST_APPLICATION_UID);
+ testUidList.add(Process.LAST_APPLICATION_UID + 1);
+ // If available, pick another existing uid for testing that is not already contained
+ // in the list above.
+ final int notMyUid = getFirstAppUidThat(uid -> uid >= 0 && !testUidList.contains(uid));
+ if (notMyUid != INVALID_UID) testUidList.add(notMyUid);
+
+ for (final int uid : testUidList) {
+ for (int i = 0; i < mUidStatsQueryOpArray.size(); i++) {
+ final int type = mUidStatsQueryOpArray.keyAt(i);
+ try {
+ final long uidStatsFromBinder = getUidStatsFromBinder(uid, type);
+ final long uidTrafficStats = mUidStatsQueryOpArray.get(type).apply(uid);
+
+ // Verify that UNSUPPORTED is returned if the uid is not current app uid.
+ if (uid != myUid) {
+ assertEquals(uidStatsFromBinder, TrafficStats.UNSUPPORTED);
+ }
+ // Verify that returned result is the same with the result get from
+ // TrafficStats.
+ // TODO: If the test is flaky then it should instead assert that the values
+ // are approximately similar.
+ assertEquals("uidStats is not matched for query type " + type
+ + ", uid=" + uid + ", myUid=" + myUid, uidTrafficStats,
+ uidStatsFromBinder);
+ } catch (IllegalAccessException e) {
+ /* Java language access prevents exploitation. */
+ return;
+ } catch (InvocationTargetException e) {
+ /* Underlying method has been changed. */
+ return;
+ } catch (ClassNotFoundException e) {
+ /* not vulnerable if hidden API no longer available */
+ return;
+ } catch (NoSuchMethodException e) {
+ /* not vulnerable if hidden API no longer available */
+ return;
+ } catch (RemoteException e) {
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
new file mode 100644
index 0000000..5290f0d
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -0,0 +1,245 @@
+/*
+ * 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 android.net.cts
+
+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
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.Uri
+import android.net.dhcp.DhcpDiscoverPacket
+import android.net.dhcp.DhcpPacket
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE
+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
+import androidx.test.runner.AndroidJUnit4
+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.DhcpClientPacketFilter
+import com.android.testutils.DhcpOptionFilter
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD.Response.Status
+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
+import java.net.Inet4Address
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_PACKET_LENGTH = 1500
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private const val TEST_LEASE_TIMEOUT_SECS = 3600 * 12
+private const val TEST_PREFIX_LENGTH = 24
+
+private const val TEST_LOGIN_URL = "https://login.capport.android.com"
+private const val TEST_VENUE_INFO_URL = "https://venueinfo.capport.android.com"
+private const val TEST_DOMAIN_NAME = "lan"
+private const val TEST_MTU = 1500.toShort()
+
+@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) }
+ private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) }
+
+ private val handlerThread = HandlerThread(NetworkValidationTest::class.java.simpleName)
+ private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address
+ private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address
+ private val httpServer = TestHttpServer()
+ private val ethRequest = NetworkRequest.Builder()
+ // ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST).build()
+ private val ethRequestCb = TestableNetworkCallback()
+
+ private lateinit var iface: TestNetworkInterface
+ private lateinit var reader: TapPacketReader
+ private lateinit var capportUrl: Uri
+
+ private var testSkipped = false
+
+ @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
+ 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()
+ }
+
+ handlerThread.start()
+ reader = TapPacketReader(
+ handlerThread.threadHandler,
+ iface.fileDescriptor.fileDescriptor,
+ MAX_PACKET_LENGTH)
+ reader.startAsyncForTest()
+ httpServer.start()
+
+ // Pad the listening port to make sure it is always of length 5. This ensures the URL has
+ // always the same length so the test can use constant IP and UDP header lengths.
+ // The maximum port number is 65535 so a length of 5 is always enough.
+ capportUrl = Uri.parse("http://localhost:${httpServer.listeningPort}/testapi.html?par=val")
+ }
+
+ @After
+ fun tearDown() {
+ if (testSkipped) return
+ cm.unregisterNetworkCallback(ethRequestCb)
+
+ runAsShell(NETWORK_SETTINGS) { eth.setIncludeTestInterfaces(false) }
+
+ httpServer.stop()
+ handlerThread.threadHandler.post { reader.stop() }
+ handlerThread.quitSafely()
+
+ iface.fileDescriptor.close()
+ }
+
+ @Test
+ fun testCapportApiCallbacks() {
+ httpServer.addResponse(capportUrl, Status.OK, content = """
+ |{
+ | "captive": true,
+ | "user-portal-url": "$TEST_LOGIN_URL",
+ | "venue-info-url": "$TEST_VENUE_INFO_URL"
+ |}
+ """.trimMargin())
+
+ // Handle the DHCP handshake that includes the capport API URL
+ val discover = reader.assertDhcpPacketReceived(
+ DhcpDiscoverPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER)
+ reader.sendResponse(makeOfferPacket(discover.clientMac, discover.transactionId))
+
+ val request = reader.assertDhcpPacketReceived(
+ DhcpRequestPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_REQUEST)
+ assertEquals(discover.transactionId, request.transactionId)
+ assertEquals(clientIpAddr, request.mRequestedIp)
+ reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId))
+
+ // The first request received by the server should be for the portal API
+ assertTrue(httpServer.requestsRecord.poll(TEST_TIMEOUT_MS, 0)?.matches(capportUrl) ?: false,
+ "The device did not fetch captive portal API data within timeout")
+
+ // Expect network callbacks with capport info
+ val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS)
+ // LinkProperties do not contain captive portal info if the callback is registered without
+ // NETWORK_SETTINGS permissions.
+ val lp = runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkCallback(ethRequest, testCb)
+
+ try {
+ val ncCb = testCb.eventuallyExpect<CallbackEntry.CapabilitiesChanged> {
+ it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
+ }
+ testCb.eventuallyExpect<CallbackEntry.LinkPropertiesChanged> {
+ it.network == ncCb.network && it.lp.captivePortalData != null
+ }.lp
+ } finally {
+ cm.unregisterNetworkCallback(testCb)
+ }
+ }
+
+ assertEquals(capportUrl, lp.captivePortalApiUrl)
+ with(lp.captivePortalData) {
+ assertNotNull(this)
+ assertTrue(isCaptive)
+ assertEquals(Uri.parse(TEST_LOGIN_URL), userPortalUrl)
+ assertEquals(Uri.parse(TEST_VENUE_INFO_URL), venueInfoUrl)
+ }
+ }
+
+ private fun makeOfferPacket(clientMac: ByteArray, transactionId: Int) =
+ DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, transactionId,
+ false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
+ clientMac, TEST_LEASE_TIMEOUT_SECS,
+ getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
+ getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
+ listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
+ serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
+ TEST_MTU, capportUrl.toString())
+
+ private fun makeAckPacket(clientMac: ByteArray, transactionId: Int) =
+ DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, transactionId,
+ false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
+ clientIpAddr /* requestClientIp */, clientMac, TEST_LEASE_TIMEOUT_SECS,
+ getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
+ getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
+ listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
+ serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
+ TEST_MTU, false /* rapidCommit */, capportUrl.toString())
+}
+
+private fun <T : DhcpPacket> TapPacketReader.assertDhcpPacketReceived(
+ packetType: Class<T>,
+ timeoutMs: Long,
+ type: Byte
+): T {
+ val packetBytes = poll(timeoutMs, DhcpClientPacketFilter()
+ .and(DhcpOptionFilter(DHCP_MESSAGE_TYPE, type)))
+ ?: fail("${packetType.simpleName} not received within timeout")
+ val packet = DhcpPacket.decodeFullPacket(packetBytes, packetBytes.size, DhcpPacket.ENCAP_L2)
+ assertTrue(packetType.isInstance(packet),
+ "Expected ${packetType.simpleName} but got ${packet.javaClass.simpleName}")
+ return packetType.cast(packet)
+}
+
+private fun <T> Context.assertHasService(manager: Class<T>): T {
+ return getSystemService(manager) ?: fail("Service $manager not found")
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
new file mode 100644
index 0000000..f6fc75b
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -0,0 +1,68 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest
+import android.net.util.NetworkStackUtils
+import android.provider.DeviceConfig
+import com.android.testutils.runAsShell
+
+/**
+ * Collection of utility methods for configuring network validation.
+ */
+internal object NetworkValidationTestUtil {
+
+ /**
+ * Clear the test network validation URLs.
+ */
+ fun clearValidationTestUrlsDeviceConfig() {
+ setHttpsUrlDeviceConfig(null)
+ setHttpUrlDeviceConfig(null)
+ setUrlExpirationDeviceConfig(null)
+ }
+
+ /**
+ * Set the test validation HTTPS URL.
+ *
+ * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+ */
+ fun setHttpsUrlDeviceConfig(url: String?) =
+ setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+
+ /**
+ * Set the test validation HTTP URL.
+ *
+ * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
+ */
+ fun setHttpUrlDeviceConfig(url: String?) =
+ setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+
+ /**
+ * Set the test validation URL expiration.
+ *
+ * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
+ */
+ fun setUrlExpirationDeviceConfig(timestamp: Long?) =
+ setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
+
+ private fun setConfig(configKey: String, value: String?) {
+ runAsShell(Manifest.permission.WRITE_DEVICE_CONFIG) {
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java
new file mode 100644
index 0000000..6833c70
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.ApiLevelUtil;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Formatter;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NetworkWatchlistTest {
+
+ private static final String TEST_WATCHLIST_XML = "assets/network_watchlist_config_for_test.xml";
+ private static final String TEST_EMPTY_WATCHLIST_XML =
+ "assets/network_watchlist_config_empty_for_test.xml";
+ private static final String TMP_CONFIG_PATH =
+ "/data/local/tmp/network_watchlist_config_for_test.xml";
+ // Generated from sha256sum network_watchlist_config_for_test.xml
+ private static final String TEST_WATCHLIST_CONFIG_HASH =
+ "B5FC4636994180D54E1E912F78178AB1D8BD2BE71D90CA9F5BBC3284E4D04ED4";
+
+ private ConnectivityManager mConnectivityManager;
+ private boolean mHasFeature;
+
+ @Before
+ public void setUp() throws Exception {
+ mHasFeature = isAtLeastP();
+ mConnectivityManager =
+ (ConnectivityManager) InstrumentationRegistry.getContext().getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ assumeTrue(mHasFeature);
+ // Set empty watchlist test config before testing
+ setWatchlistConfig(TEST_EMPTY_WATCHLIST_XML);
+ // Verify test watchlist config is not set before testing
+ byte[] result = mConnectivityManager.getNetworkWatchlistConfigHash();
+ assertNotNull("Watchlist config does not exist", result);
+ assertNotEquals(TEST_WATCHLIST_CONFIG_HASH, byteArrayToHexString(result));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mHasFeature) {
+ // Set empty watchlist test config after testing
+ setWatchlistConfig(TEST_EMPTY_WATCHLIST_XML);
+ }
+ }
+
+ private void cleanup() throws IOException {
+ runCommand("rm " + TMP_CONFIG_PATH);
+ }
+
+ private boolean isAtLeastP() throws Exception {
+ // TODO: replace with ApiLevelUtil.isAtLeast(Build.VERSION_CODES.P) when the P API level
+ // constant is defined.
+ return ApiLevelUtil.getCodename().compareToIgnoreCase("P") >= 0;
+ }
+
+ /**
+ * Test if ConnectivityManager.getNetworkWatchlistConfigHash() correctly
+ * returns the hash of config we set.
+ */
+ @Test
+ @AppModeFull(reason = "Cannot access resource file in instant app mode")
+ public void testGetWatchlistConfigHash() throws Exception {
+ // Set watchlist config file for test
+ setWatchlistConfig(TEST_WATCHLIST_XML);
+ // Test if watchlist config hash value is correct
+ byte[] result = mConnectivityManager.getNetworkWatchlistConfigHash();
+ Assert.assertEquals(TEST_WATCHLIST_CONFIG_HASH, byteArrayToHexString(result));
+ }
+
+ private static String byteArrayToHexString(byte[] bytes) {
+ Formatter formatter = new Formatter();
+ for (byte b : bytes) {
+ formatter.format("%02X", b);
+ }
+ return formatter.toString();
+ }
+
+ private void setWatchlistConfig(String watchlistConfigFile) throws Exception {
+ Log.w("NetworkWatchlistTest", "Setting watchlist config " + watchlistConfigFile
+ + " in " + Thread.currentThread().getName());
+ cleanup();
+ saveResourceToFile(watchlistConfigFile, TMP_CONFIG_PATH);
+ final String cmdResult = runCommand(
+ "cmd network_watchlist set-test-config " + TMP_CONFIG_PATH).trim();
+ assertThat(cmdResult).contains("Success");
+ cleanup();
+ }
+
+ private void saveResourceToFile(String res, String filePath) throws IOException {
+ final UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation();
+ // App can't access /data/local/tmp directly, so we pipe resource to file through stdin.
+ // Not all devices have symlink for /dev/stdin, so use /proc/self/fd/0 directly.
+ // /dev/stdin maps to /proc/self/fd/0.
+ final ParcelFileDescriptor[] fileDescriptors = uiAutomation.executeShellCommandRw(
+ "cp /proc/self/fd/0 " + filePath);
+
+ ParcelFileDescriptor stdin = fileDescriptors[1];
+ ParcelFileDescriptor stdout = fileDescriptors[0];
+
+ pipeResourceToFileDescriptor(res, stdin);
+
+ // Wait for the process to close its stdout - which should mean it has completed.
+ consumeFile(stdout);
+ }
+
+ private void consumeFile(ParcelFileDescriptor pfd) throws IOException {
+ try (InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+ for (;;) {
+ if (stream.read() == -1) {
+ return;
+ }
+ }
+ }
+ }
+
+ private void pipeResourceToFileDescriptor(String res, ParcelFileDescriptor pfd)
+ throws IOException {
+ try (InputStream resStream = getClass().getClassLoader().getResourceAsStream(res);
+ FileOutputStream fdStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd)) {
+ FileUtils.copy(resStream, fdStream);
+ }
+ }
+
+ private static String runCommand(String command) throws IOException {
+ return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
new file mode 100644
index 0000000..2bcfdc3
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.List;
+import java.util.ArrayList;
+
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+public class NsdManagerTest extends AndroidTestCase {
+
+ private static final String TAG = "NsdManagerTest";
+ private static final String SERVICE_TYPE = "_nmt._tcp";
+ private static final int TIMEOUT = 2000;
+
+ private static final boolean DBG = false;
+
+ NsdManager mNsdManager;
+
+ NsdManager.RegistrationListener mRegistrationListener;
+ NsdManager.DiscoveryListener mDiscoveryListener;
+ NsdManager.ResolveListener mResolveListener;
+ private NsdServiceInfo mResolvedService;
+
+ public NsdManagerTest() {
+ initRegistrationListener();
+ initDiscoveryListener();
+ initResolveListener();
+ }
+
+ private void initRegistrationListener() {
+ mRegistrationListener = new NsdManager.RegistrationListener() {
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ setEvent("onRegistrationFailed", errorCode);
+ }
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ setEvent("onUnregistrationFailed", errorCode);
+ }
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceRegistered", serviceInfo);
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceUnregistered", serviceInfo);
+ }
+ };
+ }
+
+ private void initDiscoveryListener() {
+ mDiscoveryListener = new NsdManager.DiscoveryListener() {
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ setEvent("onStartDiscoveryFailed", errorCode);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ setEvent("onStopDiscoveryFailed", errorCode);
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceType(serviceType);
+ setEvent("onDiscoveryStarted", info);
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceType(serviceType);
+ setEvent("onDiscoveryStopped", info);
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceFound", serviceInfo);
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceLost", serviceInfo);
+ }
+ };
+ }
+
+ private void initResolveListener() {
+ mResolveListener = new NsdManager.ResolveListener() {
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ setEvent("onResolveFailed", errorCode);
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ mResolvedService = serviceInfo;
+ setEvent("onServiceResolved", serviceInfo);
+ }
+ };
+ }
+
+
+
+ private final class EventData {
+ EventData(String callbackName, NsdServiceInfo info) {
+ mCallbackName = callbackName;
+ mSucceeded = true;
+ mErrorCode = 0;
+ mInfo = info;
+ }
+ EventData(String callbackName, int errorCode) {
+ mCallbackName = callbackName;
+ mSucceeded = false;
+ mErrorCode = errorCode;
+ mInfo = null;
+ }
+ private final String mCallbackName;
+ private final boolean mSucceeded;
+ private final int mErrorCode;
+ private final NsdServiceInfo mInfo;
+ }
+
+ private final List<EventData> mEventCache = new ArrayList<EventData>();
+
+ private void setEvent(String callbackName, int errorCode) {
+ if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode));
+ EventData eventData = new EventData(callbackName, errorCode);
+ synchronized (mEventCache) {
+ mEventCache.add(eventData);
+ mEventCache.notify();
+ }
+ }
+
+ private void setEvent(String callbackName, NsdServiceInfo info) {
+ if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName());
+ EventData eventData = new EventData(callbackName, info);
+ synchronized (mEventCache) {
+ mEventCache.add(eventData);
+ mEventCache.notify();
+ }
+ }
+
+ void clearEventCache() {
+ synchronized(mEventCache) {
+ mEventCache.clear();
+ }
+ }
+
+ int eventCacheSize() {
+ synchronized(mEventCache) {
+ return mEventCache.size();
+ }
+ }
+
+ private int mWaitId = 0;
+ private EventData waitForCallback(String callbackName) {
+
+ synchronized(mEventCache) {
+
+ mWaitId ++;
+ if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId));
+
+ try {
+ long startTime = android.os.SystemClock.uptimeMillis();
+ long elapsedTime = 0;
+ int index = 0;
+ while (elapsedTime < TIMEOUT ) {
+ // first check if we've received that event
+ for (; index < mEventCache.size(); index++) {
+ EventData e = mEventCache.get(index);
+ if (e.mCallbackName.equals(callbackName)) {
+ if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId));
+ return e;
+ }
+ }
+
+ // Not yet received, just wait
+ mEventCache.wait(TIMEOUT - elapsedTime);
+ elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
+ }
+ // we exited the loop because of TIMEOUT; fail the call
+ if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId));
+ return null;
+ } catch (InterruptedException e) {
+ return null; // wait timed out!
+ }
+ }
+ }
+
+ private EventData waitForNewEvents() throws InterruptedException {
+ if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId));
+
+ long startTime = android.os.SystemClock.uptimeMillis();
+ long elapsedTime = 0;
+ synchronized (mEventCache) {
+ int index = mEventCache.size();
+ while (elapsedTime < TIMEOUT ) {
+ // first check if we've received that event
+ for (; index < mEventCache.size(); index++) {
+ EventData e = mEventCache.get(index);
+ return e;
+ }
+
+ // Not yet received, just wait
+ mEventCache.wait(TIMEOUT - elapsedTime);
+ elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
+ }
+ }
+
+ return null;
+ }
+
+ private String mServiceName;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ if (DBG) Log.d(TAG, "Setup test ...");
+ mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE);
+
+ Random rand = new Random();
+ mServiceName = new String("NsdTest");
+ for (int i = 0; i < 4; i++) {
+ mServiceName = mServiceName + String.valueOf(rand.nextInt(10));
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (DBG) Log.d(TAG, "Tear down test ...");
+ super.tearDown();
+ }
+
+ public void testNDSManager() throws Exception {
+ EventData lastEvent = null;
+
+ if (DBG) Log.d(TAG, "Starting test ...");
+
+ NsdServiceInfo si = new NsdServiceInfo();
+ si.setServiceType(SERVICE_TYPE);
+ si.setServiceName(mServiceName);
+
+ byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2};
+ String String256 = "1_________2_________3_________4_________5_________6_________" +
+ "7_________8_________9_________10________11________12________13________" +
+ "14________15________16________17________18________19________20________" +
+ "21________22________23________24________25________123456";
+
+ // Illegal attributes
+ try {
+ si.setAttribute(null, (String) null);
+ fail("Could set null key");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("", (String) null);
+ fail("Could set empty key");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute(String256, (String) null);
+ fail("Could set key with 255 characters");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("key", String256.substring(3));
+ fail("Could set key+value combination with more than 255 characters");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("key", String256.substring(4));
+ fail("Could set key+value combination with 255 characters");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute(new String(new byte[]{0x19}), (String) null);
+ fail("Could set key with invalid character");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("=", (String) null);
+ fail("Could set key with invalid character");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute(new String(new byte[]{0x7F}), (String) null);
+ fail("Could set key with invalid character");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ // Allowed attributes
+ si.setAttribute("booleanAttr", (String) null);
+ si.setAttribute("keyValueAttr", "value");
+ si.setAttribute("keyEqualsAttr", "=");
+ si.setAttribute(" whiteSpaceKeyValueAttr ", " value ");
+ si.setAttribute("binaryDataAttr", testByteArray);
+ si.setAttribute("nullBinaryDataAttr", (byte[]) null);
+ si.setAttribute("emptyBinaryDataAttr", new byte[]{});
+ si.setAttribute("longkey", String256.substring(9));
+
+ ServerSocket socket;
+ int localPort;
+
+ try {
+ socket = new ServerSocket(0);
+ localPort = socket.getLocalPort();
+ si.setPort(localPort);
+ } catch (IOException e) {
+ if (DBG) Log.d(TAG, "Could not open a local socket");
+ assertTrue(false);
+ return;
+ }
+
+ if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort));
+
+ clearEventCache();
+
+ mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
+ lastEvent = waitForCallback("onServiceRegistered"); // id = 1
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+ assertTrue(eventCacheSize() == 1);
+
+ // We may not always get the name that we tried to register;
+ // This events tells us the name that was registered.
+ String registeredName = lastEvent.mInfo.getServiceName();
+ si.setServiceName(registeredName);
+
+ clearEventCache();
+
+ mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ mDiscoveryListener);
+
+ // Expect discovery started
+ lastEvent = waitForCallback("onDiscoveryStarted"); // id = 2
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ // Remove this event, so accounting becomes easier later
+ synchronized (mEventCache) {
+ mEventCache.remove(lastEvent);
+ }
+
+ // Expect a service record to be discovered (and filter the ones
+ // that are unrelated to this test)
+ boolean found = false;
+ for (int i = 0; i < 32; i++) {
+
+ lastEvent = waitForCallback("onServiceFound"); // id = 3
+ if (lastEvent == null) {
+ // no more onServiceFound events are being reported!
+ break;
+ }
+
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
+ lastEvent.mInfo.getServiceName());
+
+ if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
+ // Save it, as it will get overwritten with new serviceFound events
+ si = lastEvent.mInfo;
+ found = true;
+ }
+
+ // Remove this event from the event cache, so it won't be found by subsequent
+ // calls to waitForCallback
+ synchronized (mEventCache) {
+ mEventCache.remove(lastEvent);
+ }
+ }
+
+ assertTrue(found);
+
+ // We've removed all serviceFound events, and we've removed the discoveryStarted
+ // event as well, so now the event cache should be empty!
+ assertTrue(eventCacheSize() == 0);
+
+ // Resolve the service
+ clearEventCache();
+ mNsdManager.resolveService(si, mResolveListener);
+ lastEvent = waitForCallback("onServiceResolved"); // id = 4
+
+ assertNotNull(mResolvedService);
+
+ // Check Txt attributes
+ assertEquals(8, mResolvedService.getAttributes().size());
+ assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr"));
+ assertNull(mResolvedService.getAttributes().get("booleanAttr"));
+ assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr")));
+ assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr")));
+ assertEquals(" value ", new String(mResolvedService.getAttributes()
+ .get(" whiteSpaceKeyValueAttr ")));
+ assertEquals(String256.substring(9), new String(mResolvedService.getAttributes()
+ .get("longkey")));
+ assertTrue(Arrays.equals(testByteArray,
+ mResolvedService.getAttributes().get("binaryDataAttr")));
+ assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr"));
+ assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr"));
+ assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr"));
+ assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr"));
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " +
+ String.valueOf(lastEvent.mInfo.getPort()));
+
+ assertTrue(lastEvent.mInfo.getPort() == localPort);
+ assertTrue(eventCacheSize() == 1);
+
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ // Unregister the service
+ mNsdManager.unregisterService(mRegistrationListener);
+ lastEvent = waitForCallback("onServiceUnregistered"); // id = 5
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ // Expect a callback for service lost
+ lastEvent = waitForCallback("onServiceLost"); // id = 6
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
+
+ // Register service again to see if we discover it
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ si = new NsdServiceInfo();
+ si.setServiceType(SERVICE_TYPE);
+ si.setServiceName(mServiceName);
+ si.setPort(localPort);
+
+ // Create a new registration listener and register same service again
+ initRegistrationListener();
+
+ mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
+
+ lastEvent = waitForCallback("onServiceRegistered"); // id = 7
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ registeredName = lastEvent.mInfo.getServiceName();
+
+ // Expect a record to be discovered
+ // Expect a service record to be discovered (and filter the ones
+ // that are unrelated to this test)
+ found = false;
+ for (int i = 0; i < 32; i++) {
+
+ lastEvent = waitForCallback("onServiceFound"); // id = 8
+ if (lastEvent == null) {
+ // no more onServiceFound events are being reported!
+ break;
+ }
+
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
+ lastEvent.mInfo.getServiceName());
+
+ if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
+ // Save it, as it will get overwritten with new serviceFound events
+ si = lastEvent.mInfo;
+ found = true;
+ }
+
+ // Remove this event from the event cache, so it won't be found by subsequent
+ // calls to waitForCallback
+ synchronized (mEventCache) {
+ mEventCache.remove(lastEvent);
+ }
+ }
+
+ assertTrue(found);
+
+ // Resolve the service
+ clearEventCache();
+ mNsdManager.resolveService(si, mResolveListener);
+ lastEvent = waitForCallback("onServiceResolved"); // id = 9
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
+ lastEvent.mInfo.getServiceName());
+
+ assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
+
+ assertNotNull(mResolvedService);
+
+ // Check that we don't have any TXT records
+ assertEquals(0, mResolvedService.getAttributes().size());
+
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ mNsdManager.stopServiceDiscovery(mDiscoveryListener);
+ lastEvent = waitForCallback("onDiscoveryStopped"); // id = 10
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+ assertTrue(checkCacheSize(1));
+
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ mNsdManager.unregisterService(mRegistrationListener);
+
+ lastEvent = waitForCallback("onServiceUnregistered"); // id = 11
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+ assertTrue(checkCacheSize(1));
+ }
+
+ boolean checkCacheSize(int size) {
+ synchronized (mEventCache) {
+ int cacheSize = mEventCache.size();
+ if (cacheSize != size) {
+ Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize);
+ for (int i = 0; i < cacheSize; i++) {
+ EventData e = mEventCache.get(i);
+ String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
+ Log.d(TAG, "eventName is " + e.mCallbackName + sname);
+ }
+ }
+ return (cacheSize == size);
+ }
+ }
+
+ boolean checkForAdditionalEvents() {
+ try {
+ EventData e = waitForNewEvents();
+ if (e != null) {
+ String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
+ Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname);
+ }
+ return (e == null);
+ }
+ catch (InterruptedException ex) {
+ return false;
+ }
+ }
+}
+
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
new file mode 100644
index 0000000..0aedecb
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PacketUtils {
+ private static final String TAG = PacketUtils.class.getSimpleName();
+
+ private static final int DATA_BUFFER_LEN = 4096;
+
+ static final int IP4_HDRLEN = 20;
+ static final int IP6_HDRLEN = 40;
+ static final int UDP_HDRLEN = 8;
+ static final int TCP_HDRLEN = 20;
+ static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
+
+ // Not defined in OsConstants
+ static final int IPPROTO_IPV4 = 4;
+ static final int IPPROTO_ESP = 50;
+
+ // Encryption parameters
+ static final int AES_GCM_IV_LEN = 8;
+ static final int AES_CBC_IV_LEN = 16;
+ static final int AES_GCM_BLK_SIZE = 4;
+ static final int AES_CBC_BLK_SIZE = 16;
+
+ // Encryption algorithms
+ static final String AES = "AES";
+ static final String AES_CBC = "AES/CBC/NoPadding";
+ static final String HMAC_SHA_256 = "HmacSHA256";
+
+ public interface Payload {
+ byte[] getPacketBytes(IpHeader header) throws Exception;
+
+ void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception;
+
+ short length();
+
+ int getProtocolId();
+ }
+
+ public abstract static class IpHeader {
+
+ public final byte proto;
+ public final InetAddress srcAddr;
+ public final InetAddress dstAddr;
+ public final Payload payload;
+
+ public IpHeader(int proto, InetAddress src, InetAddress dst, Payload payload) {
+ this.proto = (byte) proto;
+ this.srcAddr = src;
+ this.dstAddr = dst;
+ this.payload = payload;
+ }
+
+ public abstract byte[] getPacketBytes() throws Exception;
+
+ public abstract int getProtocolId();
+ }
+
+ public static class Ip4Header extends IpHeader {
+ private short checksum;
+
+ public Ip4Header(int proto, Inet4Address src, Inet4Address dst, Payload payload) {
+ super(proto, src, dst, payload);
+ }
+
+ public byte[] getPacketBytes() throws Exception {
+ ByteBuffer resultBuffer = buildHeader();
+ payload.addPacketBytes(this, resultBuffer);
+
+ return getByteArrayFromBuffer(resultBuffer);
+ }
+
+ public ByteBuffer buildHeader() {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ // Version, IHL
+ bb.put((byte) (0x45));
+
+ // DCSP, ECN
+ bb.put((byte) 0);
+
+ // Total Length
+ bb.putShort((short) (IP4_HDRLEN + payload.length()));
+
+ // Empty for Identification, Flags and Fragment Offset
+ bb.putShort((short) 0);
+ bb.put((byte) 0x40);
+ bb.put((byte) 0x00);
+
+ // TTL
+ bb.put((byte) 64);
+
+ // Protocol
+ bb.put(proto);
+
+ // Header Checksum
+ final int ipChecksumOffset = bb.position();
+ bb.putShort((short) 0);
+
+ // Src/Dst addresses
+ bb.put(srcAddr.getAddress());
+ bb.put(dstAddr.getAddress());
+
+ bb.putShort(ipChecksumOffset, calculateChecksum(bb));
+
+ return bb;
+ }
+
+ private short calculateChecksum(ByteBuffer bb) {
+ int checksum = 0;
+
+ // Calculate sum of 16-bit values, excluding checksum. IPv4 headers are always 32-bit
+ // aligned, so no special cases needed for unaligned values.
+ ShortBuffer shortBuffer = ByteBuffer.wrap(getByteArrayFromBuffer(bb)).asShortBuffer();
+ while (shortBuffer.hasRemaining()) {
+ short val = shortBuffer.get();
+
+ // Wrap as needed
+ checksum = addAndWrapForChecksum(checksum, val);
+ }
+
+ return onesComplement(checksum);
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_IPV4;
+ }
+ }
+
+ public static class Ip6Header extends IpHeader {
+ public Ip6Header(int nextHeader, Inet6Address src, Inet6Address dst, Payload payload) {
+ super(nextHeader, src, dst, payload);
+ }
+
+ public byte[] getPacketBytes() throws Exception {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ // Version | Traffic Class (First 4 bits)
+ bb.put((byte) 0x60);
+
+ // Traffic class (Last 4 bits), Flow Label
+ bb.put((byte) 0);
+ bb.put((byte) 0);
+ bb.put((byte) 0);
+
+ // Payload Length
+ bb.putShort((short) payload.length());
+
+ // Next Header
+ bb.put(proto);
+
+ // Hop Limit
+ bb.put((byte) 64);
+
+ // Src/Dst addresses
+ bb.put(srcAddr.getAddress());
+ bb.put(dstAddr.getAddress());
+
+ // Payload
+ payload.addPacketBytes(this, bb);
+
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_IPV6;
+ }
+ }
+
+ public static class BytePayload implements Payload {
+ public final byte[] payload;
+
+ public BytePayload(byte[] payload) {
+ this.payload = payload;
+ }
+
+ public int getProtocolId() {
+ return -1;
+ }
+
+ public byte[] getPacketBytes(IpHeader header) {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ addPacketBytes(header, bb);
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) {
+ resultBuffer.put(payload);
+ }
+
+ public short length() {
+ return (short) payload.length;
+ }
+ }
+
+ public static class UdpHeader implements Payload {
+
+ public final short srcPort;
+ public final short dstPort;
+ public final Payload payload;
+
+ public UdpHeader(int srcPort, int dstPort, Payload payload) {
+ this.srcPort = (short) srcPort;
+ this.dstPort = (short) dstPort;
+ this.payload = payload;
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_UDP;
+ }
+
+ public short length() {
+ return (short) (payload.length() + 8);
+ }
+
+ public byte[] getPacketBytes(IpHeader header) throws Exception {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ addPacketBytes(header, bb);
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception {
+ // Source, Destination port
+ resultBuffer.putShort(srcPort);
+ resultBuffer.putShort(dstPort);
+
+ // Payload Length
+ resultBuffer.putShort(length());
+
+ // Get payload bytes for checksum + payload
+ ByteBuffer payloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
+ payload.addPacketBytes(header, payloadBuffer);
+ byte[] payloadBytes = getByteArrayFromBuffer(payloadBuffer);
+
+ // Checksum
+ resultBuffer.putShort(calculateChecksum(header, payloadBytes));
+
+ // Payload
+ resultBuffer.put(payloadBytes);
+ }
+
+ private short calculateChecksum(IpHeader header, byte[] payloadBytes) throws Exception {
+ int newChecksum = 0;
+ ShortBuffer srcBuffer = ByteBuffer.wrap(header.srcAddr.getAddress()).asShortBuffer();
+ ShortBuffer dstBuffer = ByteBuffer.wrap(header.dstAddr.getAddress()).asShortBuffer();
+
+ while (srcBuffer.hasRemaining() || dstBuffer.hasRemaining()) {
+ short val = srcBuffer.hasRemaining() ? srcBuffer.get() : dstBuffer.get();
+
+ // Wrap as needed
+ newChecksum = addAndWrapForChecksum(newChecksum, val);
+ }
+
+ // Add pseudo-header values. Proto is 0-padded, so just use the byte.
+ newChecksum = addAndWrapForChecksum(newChecksum, header.proto);
+ newChecksum = addAndWrapForChecksum(newChecksum, length());
+ newChecksum = addAndWrapForChecksum(newChecksum, srcPort);
+ newChecksum = addAndWrapForChecksum(newChecksum, dstPort);
+ newChecksum = addAndWrapForChecksum(newChecksum, length());
+
+ ShortBuffer payloadShortBuffer = ByteBuffer.wrap(payloadBytes).asShortBuffer();
+ while (payloadShortBuffer.hasRemaining()) {
+ newChecksum = addAndWrapForChecksum(newChecksum, payloadShortBuffer.get());
+ }
+ if (payload.length() % 2 != 0) {
+ newChecksum =
+ addAndWrapForChecksum(
+ newChecksum, (payloadBytes[payloadBytes.length - 1] << 8));
+ }
+
+ return onesComplement(newChecksum);
+ }
+ }
+
+ public static class EspHeader implements Payload {
+ public final int nextHeader;
+ public final int spi;
+ public final int seqNum;
+ public final byte[] key;
+ public final byte[] payload;
+
+ /**
+ * Generic constructor for ESP headers.
+ *
+ * <p>For Tunnel mode, payload will be a full IP header + attached payloads
+ *
+ * <p>For Transport mode, payload will be only the attached payloads, but with the checksum
+ * calculated using the pre-encryption IP header
+ */
+ public EspHeader(int nextHeader, int spi, int seqNum, byte[] key, byte[] payload) {
+ this.nextHeader = nextHeader;
+ this.spi = spi;
+ this.seqNum = seqNum;
+ this.key = key;
+ this.payload = payload;
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_ESP;
+ }
+
+ public short length() {
+ // ALWAYS uses AES-CBC, HMAC-SHA256 (128b trunc len)
+ return (short)
+ calculateEspPacketSize(payload.length, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, 128);
+ }
+
+ public byte[] getPacketBytes(IpHeader header) throws Exception {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ addPacketBytes(header, bb);
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception {
+ ByteBuffer espPayloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
+ espPayloadBuffer.putInt(spi);
+ espPayloadBuffer.putInt(seqNum);
+ espPayloadBuffer.put(getCiphertext(key));
+
+ espPayloadBuffer.put(getIcv(getByteArrayFromBuffer(espPayloadBuffer)), 0, 16);
+ resultBuffer.put(getByteArrayFromBuffer(espPayloadBuffer));
+ }
+
+ private byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+ Mac sha256HMAC = Mac.getInstance(HMAC_SHA_256);
+ SecretKeySpec authKey = new SecretKeySpec(key, HMAC_SHA_256);
+ sha256HMAC.init(authKey);
+
+ return sha256HMAC.doFinal(authenticatedSection);
+ }
+
+ /**
+ * Encrypts and builds ciphertext block. Includes the IV, Padding and Next-Header blocks
+ *
+ * <p>The ciphertext does NOT include the SPI/Sequence numbers, or the ICV.
+ */
+ private byte[] getCiphertext(byte[] key) throws GeneralSecurityException {
+ int paddedLen = calculateEspEncryptedLength(payload.length, AES_CBC_BLK_SIZE);
+ ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
+ paddedPayload.put(payload);
+
+ // Add padding - consecutive integers from 0x01
+ int pad = 1;
+ while (paddedPayload.position() < paddedPayload.limit()) {
+ paddedPayload.put((byte) pad++);
+ }
+
+ paddedPayload.position(paddedPayload.limit() - 2);
+ paddedPayload.put((byte) (paddedLen - 2 - payload.length)); // Pad length
+ paddedPayload.put((byte) nextHeader);
+
+ // Generate Initialization Vector
+ byte[] iv = new byte[AES_CBC_IV_LEN];
+ new SecureRandom().nextBytes(iv);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key, AES);
+
+ // Encrypt payload
+ Cipher cipher = Cipher.getInstance(AES_CBC);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+ byte[] encrypted = cipher.doFinal(getByteArrayFromBuffer(paddedPayload));
+
+ // Build ciphertext
+ ByteBuffer cipherText = ByteBuffer.allocate(AES_CBC_IV_LEN + encrypted.length);
+ cipherText.put(iv);
+ cipherText.put(encrypted);
+
+ return getByteArrayFromBuffer(cipherText);
+ }
+ }
+
+ private static int addAndWrapForChecksum(int currentChecksum, int value) {
+ currentChecksum += value & 0x0000ffff;
+
+ // Wrap anything beyond the first 16 bits, and add to lower order bits
+ return (currentChecksum >>> 16) + (currentChecksum & 0x0000ffff);
+ }
+
+ private static short onesComplement(int val) {
+ val = (val >>> 16) + (val & 0xffff);
+
+ if (val == 0) return 0;
+ return (short) ((~val) & 0xffff);
+ }
+
+ public static int calculateEspPacketSize(
+ int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) {
+ final int ESP_HDRLEN = 4 + 4; // SPI + Seq#
+ final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length
+ payloadLen += cryptIvLength; // Initialization Vector
+
+ // Align to block size of encryption algorithm
+ payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize);
+ return payloadLen + ESP_HDRLEN + ICV_LEN;
+ }
+
+ private static int calculateEspEncryptedLength(int payloadLen, int cryptBlockSize) {
+ payloadLen += 2; // ESP trailer
+
+ // Align to block size of encryption algorithm
+ return payloadLen + calculateEspPadLen(payloadLen, cryptBlockSize);
+ }
+
+ private static int calculateEspPadLen(int payloadLen, int cryptBlockSize) {
+ return (cryptBlockSize - (payloadLen % cryptBlockSize)) % cryptBlockSize;
+ }
+
+ private static byte[] getByteArrayFromBuffer(ByteBuffer buffer) {
+ return Arrays.copyOfRange(buffer.array(), 0, buffer.position());
+ }
+
+ public static IpHeader getIpHeader(
+ int protocol, InetAddress src, InetAddress dst, Payload payload) {
+ if ((src instanceof Inet6Address) != (dst instanceof Inet6Address)) {
+ throw new IllegalArgumentException("Invalid src/dst address combination");
+ }
+
+ if (src instanceof Inet6Address) {
+ return new Ip6Header(protocol, (Inet6Address) src, (Inet6Address) dst, payload);
+ } else {
+ return new Ip4Header(protocol, (Inet4Address) src, (Inet4Address) dst, payload);
+ }
+ }
+
+ /*
+ * Debug printing
+ */
+ private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+ public static String bytesToHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(hexArray[b >>> 4]);
+ sb.append(hexArray[b & 0x0F]);
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/ProxyInfoTest.java b/tests/cts/net/src/android/net/cts/ProxyInfoTest.java
new file mode 100644
index 0000000..1c5624c
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ProxyInfoTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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 android.net.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+public final class ProxyInfoTest {
+ private static final String TEST_HOST = "test.example.com";
+ private static final int TEST_PORT = 5566;
+ private static final Uri TEST_URI = Uri.parse("https://test.example.com");
+ // This matches android.net.ProxyInfo#LOCAL_EXCL_LIST
+ private static final String LOCAL_EXCL_LIST = "";
+ // This matches android.net.ProxyInfo#LOCAL_HOST
+ private static final String LOCAL_HOST = "localhost";
+ // This matches android.net.ProxyInfo#LOCAL_PORT
+ private static final int LOCAL_PORT = -1;
+
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ @Test
+ public void testConstructor() {
+ final ProxyInfo proxy = new ProxyInfo((ProxyInfo) null);
+ checkEmpty(proxy);
+
+ assertEquals(proxy, new ProxyInfo(proxy));
+ }
+
+ @Test
+ public void testBuildDirectProxy() {
+ final ProxyInfo proxy1 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT);
+
+ assertEquals(TEST_HOST, proxy1.getHost());
+ assertEquals(TEST_PORT, proxy1.getPort());
+ assertArrayEquals(new String[0], proxy1.getExclusionList());
+ assertEquals(Uri.EMPTY, proxy1.getPacFileUrl());
+
+ final List<String> exclList = new ArrayList<>();
+ exclList.add("localhost");
+ exclList.add("*.exclusion.com");
+ final ProxyInfo proxy2 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT, exclList);
+
+ assertEquals(TEST_HOST, proxy2.getHost());
+ assertEquals(TEST_PORT, proxy2.getPort());
+ assertArrayEquals(exclList.toArray(new String[0]), proxy2.getExclusionList());
+ assertEquals(Uri.EMPTY, proxy2.getPacFileUrl());
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testBuildPacProxy() {
+ final ProxyInfo proxy1 = ProxyInfo.buildPacProxy(TEST_URI);
+
+ assertEquals(LOCAL_HOST, proxy1.getHost());
+ assertEquals(LOCAL_PORT, proxy1.getPort());
+ assertArrayEquals(LOCAL_EXCL_LIST.toLowerCase(Locale.ROOT).split(","),
+ proxy1.getExclusionList());
+ assertEquals(TEST_URI, proxy1.getPacFileUrl());
+
+ final ProxyInfo proxy2 = ProxyInfo.buildPacProxy(TEST_URI, TEST_PORT);
+
+ assertEquals(LOCAL_HOST, proxy2.getHost());
+ assertEquals(TEST_PORT, proxy2.getPort());
+ assertArrayEquals(LOCAL_EXCL_LIST.toLowerCase(Locale.ROOT).split(","),
+ proxy2.getExclusionList());
+ assertEquals(TEST_URI, proxy2.getPacFileUrl());
+ }
+
+ @Test
+ public void testIsValid() {
+ final ProxyInfo proxy1 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT);
+ assertTrue(proxy1.isValid());
+
+ // Given empty host
+ final ProxyInfo proxy2 = ProxyInfo.buildDirectProxy("", TEST_PORT);
+ assertFalse(proxy2.isValid());
+ // Given invalid host
+ final ProxyInfo proxy3 = ProxyInfo.buildDirectProxy(".invalid.com", TEST_PORT);
+ assertFalse(proxy3.isValid());
+ // Given invalid port.
+ final ProxyInfo proxy4 = ProxyInfo.buildDirectProxy(TEST_HOST, 0);
+ assertFalse(proxy4.isValid());
+ // Given another invalid port
+ final ProxyInfo proxy5 = ProxyInfo.buildDirectProxy(TEST_HOST, 65536);
+ assertFalse(proxy5.isValid());
+ // Given invalid exclusion list
+ final List<String> exclList = new ArrayList<>();
+ exclList.add(".invalid.com");
+ exclList.add("%.test.net");
+ final ProxyInfo proxy6 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT, exclList);
+ assertFalse(proxy6.isValid());
+ }
+
+ private void checkEmpty(ProxyInfo proxy) {
+ assertNull(proxy.getHost());
+ assertEquals(0, proxy.getPort());
+ assertNull(proxy.getExclusionList());
+ assertEquals(Uri.EMPTY, proxy.getPacFileUrl());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.java b/tests/cts/net/src/android/net/cts/ProxyTest.java
new file mode 100644
index 0000000..467d12f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ProxyTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+
+import android.net.Proxy;
+import android.test.AndroidTestCase;
+
+public class ProxyTest extends AndroidTestCase {
+
+ public void testConstructor() {
+ new Proxy();
+ }
+
+ public void testAccessProperties() {
+ final int minValidPort = 0;
+ final int maxValidPort = 65535;
+ int defaultPort = Proxy.getDefaultPort();
+ if(null == Proxy.getDefaultHost()) {
+ assertEquals(-1, defaultPort);
+ } else {
+ assertTrue(defaultPort >= minValidPort && defaultPort <= maxValidPort);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/RssiCurveTest.java b/tests/cts/net/src/android/net/cts/RssiCurveTest.java
new file mode 100644
index 0000000..d651b71
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/RssiCurveTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.net.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.RssiCurve;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** CTS tests for {@link RssiCurve}. */
+@RunWith(AndroidJUnit4.class)
+public class RssiCurveTest {
+
+ @Test
+ public void lookupScore_constantCurve() {
+ // One bucket from rssi=-100 to 100 with score 10.
+ RssiCurve curve = new RssiCurve(-100, 200, new byte[] { 10 });
+ assertThat(curve.lookupScore(-200)).isEqualTo(10);
+ assertThat(curve.lookupScore(-100)).isEqualTo(10);
+ assertThat(curve.lookupScore(0)).isEqualTo(10);
+ assertThat(curve.lookupScore(100)).isEqualTo(10);
+ assertThat(curve.lookupScore(200)).isEqualTo(10);
+ }
+
+ @Test
+ public void lookupScore_changingCurve() {
+ // One bucket from -100 to 0 with score -10, and one bucket from 0 to 100 with score 10.
+ RssiCurve curve = new RssiCurve(-100, 100, new byte[] { -10, 10 });
+ assertThat(curve.lookupScore(-200)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-100)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-50)).isEqualTo(-10);
+ assertThat(curve.lookupScore(0)).isEqualTo(10);
+ assertThat(curve.lookupScore(50)).isEqualTo(10);
+ assertThat(curve.lookupScore(100)).isEqualTo(10);
+ assertThat(curve.lookupScore(200)).isEqualTo(10);
+ }
+
+ @Test
+ public void lookupScore_linearCurve() {
+ // Curve starting at -110, with 15 buckets of width 10 whose scores increases by 10 with
+ // each bucket. The current active network gets a boost of 15 to its RSSI.
+ RssiCurve curve = new RssiCurve(
+ -110,
+ 10,
+ new byte[] { -20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 },
+ 15);
+
+ assertThat(curve.lookupScore(-120)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-120, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-120, true)).isEqualTo(-20);
+
+ assertThat(curve.lookupScore(-111)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-111, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-111, true)).isEqualTo(-10);
+
+ assertThat(curve.lookupScore(-110)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-110, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-110, true)).isEqualTo(-10);
+
+ assertThat(curve.lookupScore(-105)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-105, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-105, true)).isEqualTo(0);
+
+ assertThat(curve.lookupScore(-100)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-100, false)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-100, true)).isEqualTo(0);
+
+ assertThat(curve.lookupScore(-50)).isEqualTo(40);
+ assertThat(curve.lookupScore(-50, false)).isEqualTo(40);
+ assertThat(curve.lookupScore(-50, true)).isEqualTo(50);
+
+ assertThat(curve.lookupScore(0)).isEqualTo(90);
+ assertThat(curve.lookupScore(0, false)).isEqualTo(90);
+ assertThat(curve.lookupScore(0, true)).isEqualTo(100);
+
+ assertThat(curve.lookupScore(30)).isEqualTo(120);
+ assertThat(curve.lookupScore(30, false)).isEqualTo(120);
+ assertThat(curve.lookupScore(30, true)).isEqualTo(120);
+
+ assertThat(curve.lookupScore(40)).isEqualTo(120);
+ assertThat(curve.lookupScore(40, false)).isEqualTo(120);
+ assertThat(curve.lookupScore(40, true)).isEqualTo(120);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java b/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java
new file mode 100644
index 0000000..cbe54f8
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.SSLCertificateSocketFactory;
+import android.platform.test.annotations.AppModeFull;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import libcore.javax.net.ssl.SSLConfigurationAsserts;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SSLCertificateSocketFactoryTest {
+ // TEST_HOST should point to a web server with a valid TLS certificate.
+ private static final String TEST_HOST = "www.google.com";
+ private static final int HTTPS_PORT = 443;
+ private HostnameVerifier mDefaultVerifier;
+ private SSLCertificateSocketFactory mSocketFactory;
+ private InetAddress mLocalAddress;
+ // InetAddress obtained by resolving TEST_HOST.
+ private InetAddress mTestHostAddress;
+ // SocketAddress combining mTestHostAddress and HTTPS_PORT.
+ private List<SocketAddress> mTestSocketAddresses;
+
+ @Before
+ public void setUp() {
+ // Expected state before each test method is that
+ // HttpsURLConnection.getDefaultHostnameVerifier() will return the system default.
+ mDefaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+ mSocketFactory = (SSLCertificateSocketFactory)
+ SSLCertificateSocketFactory.getDefault(1000 /* handshakeTimeoutMillis */);
+ assertNotNull(mSocketFactory);
+ InetAddress[] addresses;
+ try {
+ addresses = InetAddress.getAllByName(TEST_HOST);
+ mTestHostAddress = addresses[0];
+ } catch (UnknownHostException uhe) {
+ throw new AssertionError(
+ "Unable to test SSLCertificateSocketFactory: cannot resolve " + TEST_HOST, uhe);
+ }
+
+ mTestSocketAddresses = Arrays.stream(addresses)
+ .map(addr -> new InetSocketAddress(addr, HTTPS_PORT))
+ .collect(Collectors.toList());
+
+ // Find the local IP address which will be used to connect to TEST_HOST.
+ try {
+ Socket testSocket = new Socket(TEST_HOST, HTTPS_PORT);
+ mLocalAddress = testSocket.getLocalAddress();
+ testSocket.close();
+ } catch (IOException ioe) {
+ throw new AssertionError(""
+ + "Unable to test SSLCertificateSocketFactory: cannot connect to "
+ + TEST_HOST, ioe);
+ }
+ }
+
+ // Restore the system default hostname verifier after each test.
+ @After
+ public void restoreDefaultHostnameVerifier() {
+ HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
+ }
+
+ @Test
+ public void testDefaultConfiguration() throws Exception {
+ SSLConfigurationAsserts.assertSSLSocketFactoryDefaultConfiguration(mSocketFactory);
+ }
+
+ @Test
+ public void testAccessProperties() {
+ mSocketFactory.getSupportedCipherSuites();
+ mSocketFactory.getDefaultCipherSuites();
+ }
+
+ /**
+ * Tests the {@code createSocket()} cases which are expected to fail with {@code IOException}.
+ */
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void createSocket_io_error_expected() {
+ // Connect to the localhost HTTPS port. Should result in connection refused IOException
+ // because no service should be listening on that port.
+ InetAddress localhostAddress = InetAddress.getLoopbackAddress();
+ try {
+ mSocketFactory.createSocket(localhostAddress, HTTPS_PORT);
+ fail();
+ } catch (IOException e) {
+ // expected
+ }
+
+ // Same, but also binding to a local address.
+ try {
+ mSocketFactory.createSocket(localhostAddress, HTTPS_PORT, localhostAddress, 0);
+ fail();
+ } catch (IOException e) {
+ // expected
+ }
+
+ // Same, wrapping an existing plain socket which is in an unconnected state.
+ try {
+ Socket socket = new Socket();
+ mSocketFactory.createSocket(socket, "localhost", HTTPS_PORT, true);
+ fail();
+ } catch (IOException e) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(String, int)}.
+ *
+ * <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
+ * and whose peer TLS certificate has been verified to have the correct hostname.
+ *
+ * <p>{@link SSLCertificateSocketFactory} is documented to verify hostnames using
+ * the {@link HostnameVerifier} returned by
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier}, so this test connects twice,
+ * once with the system default {@link HostnameVerifier} which is expected to succeed,
+ * and once after installing a {@link NegativeHostnameVerifier} which will cause
+ * {@link SSLCertificateSocketFactory#verifyHostname} to throw a
+ * {@link SSLPeerUnverifiedException}.
+ *
+ * <p>These tests only test the hostname verification logic in SSLCertificateSocketFactory,
+ * other TLS failure modes and the default HostnameVerifier are tested elsewhere, see
+ * {@link com.squareup.okhttp.internal.tls.HostnameVerifierTest} and
+ * https://android.googlesource.com/platform/external/boringssl/+/refs/heads/master/src/ssl/test
+ *
+ * <p>Tests the following behaviour:-
+ * <ul>
+ * <li>TEST_SERVER is available and has a valid TLS certificate
+ * <li>{@code createSocket()} verifies the remote hostname is correct using
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier}
+ * <li>{@link SSLPeerUnverifiedException} is thrown when the remote hostname is invalid
+ * </ul>
+ *
+ * <p>See also http://b/2807618.
+ */
+ @Test
+ public void createSocket_simple_with_hostname_verification() throws Exception {
+ Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT);
+ assertConnectedSocket(socket);
+ socket.close();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ try {
+ mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(Socket, String, int, boolean)}.
+ *
+ * <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
+ * and whose peer TLS certificate has been verified to have the correct hostname.
+ *
+ * <p>The TLS socket returned is wrapped around the plain socket passed into
+ * {@code createSocket()}.
+ *
+ * <p>See {@link #createSocket_simple_with_hostname_verification()} for test methodology.
+ */
+ @Test
+ public void createSocket_wrapped_with_hostname_verification() throws Exception {
+ Socket underlying = new Socket(TEST_HOST, HTTPS_PORT);
+ Socket socket = mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true);
+ assertConnectedSocket(socket);
+ socket.close();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ try {
+ underlying = new Socket(TEST_HOST, HTTPS_PORT);
+ mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(String, int, InetAddress, int)}.
+ *
+ * <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
+ * and whose peer TLS certificate has been verified to have the correct hostname.
+ *
+ * <p>The TLS socket returned is also bound to the local address determined in {@link #setUp} to
+ * be used for connections to TEST_HOST, and a wildcard port.
+ *
+ * <p>See {@link #createSocket_simple_with_hostname_verification()} for test methodology.
+ */
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void createSocket_bound_with_hostname_verification() throws Exception {
+ Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0);
+ assertConnectedSocket(socket);
+ socket.close();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ try {
+ mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(InetAddress, int)}.
+ *
+ * <p>This method should return a socket which the documentation describes as "unconnected",
+ * which actually means that the socket is fully connected at the TCP layer but TLS handshaking
+ * and hostname verification have not yet taken place.
+ *
+ * <p>Behaviour is tested by installing a {@link NegativeHostnameVerifier} and by calling
+ * {@link #assertConnectedSocket} to ensure TLS handshaking but no hostname verification takes
+ * place. Next, {@link SSLCertificateSocketFactory#verifyHostname} is called to ensure
+ * that hostname verification is using the {@link HostnameVerifier} returned by
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier} as documented.
+ *
+ * <p>Tests the following behaviour:-
+ * <ul>
+ * <li>TEST_SERVER is available and has a valid TLS certificate
+ * <li>{@code createSocket()} does not verify the remote hostname
+ * <li>Calling {@link SSLCertificateSocketFactory#verifyHostname} on the returned socket
+ * throws {@link SSLPeerUnverifiedException} if the remote hostname is invalid
+ * </ul>
+ */
+ @Test
+ public void createSocket_simple_no_hostname_verification() throws Exception{
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ Socket socket = mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT);
+ // Need to provide the expected hostname here or the TLS handshake will
+ // be unable to supply SNI to the remote host.
+ mSocketFactory.setHostname(socket, TEST_HOST);
+ assertConnectedSocket(socket);
+ try {
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ socket.close();
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(InetAddress, int, InetAddress, int)}.
+ *
+ * <p>This method should return a socket which the documentation describes as "unconnected",
+ * which actually means that the socket is fully connected at the TCP layer but TLS handshaking
+ * and hostname verification have not yet taken place.
+ *
+ * <p>The TLS socket returned is also bound to the local address determined in {@link #setUp} to
+ * be used for connections to TEST_HOST, and a wildcard port.
+ *
+ * <p>See {@link #createSocket_simple_no_hostname_verification()} for test methodology.
+ */
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void createSocket_bound_no_hostname_verification() throws Exception{
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ Socket socket =
+ mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT, mLocalAddress, 0);
+ // Need to provide the expected hostname here or the TLS handshake will
+ // be unable to supply SNI to the peer.
+ mSocketFactory.setHostname(socket, TEST_HOST);
+ assertConnectedSocket(socket);
+ try {
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ socket.close();
+ }
+
+ /**
+ * Asserts a socket is fully connected to the expected peer.
+ *
+ * <p>For the variants of createSocket which verify the remote hostname,
+ * {@code socket} should already be fully connected.
+ *
+ * <p>For the non-verifying variants, retrieving the input stream will trigger a TLS handshake
+ * and so may throw an exception, for example if the peer's certificate is invalid.
+ *
+ * <p>Does no hostname verification.
+ */
+ private void assertConnectedSocket(Socket socket) throws Exception {
+ assertNotNull(socket);
+ assertTrue(socket.isConnected());
+ assertNotNull(socket.getInputStream());
+ assertNotNull(socket.getOutputStream());
+ assertTrue(mTestSocketAddresses.contains(socket.getRemoteSocketAddress()));
+ }
+
+ /**
+ * A HostnameVerifier which always returns false to simulate a server returning a
+ * certificate which does not match the expected hostname.
+ */
+ private static class NegativeHostnameVerifier implements HostnameVerifier {
+ @Override
+ public boolean verify(String hostname, SSLSession sslSession) {
+ return false;
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TestUtils.java b/tests/cts/net/src/android/net/cts/TestUtils.java
new file mode 100644
index 0000000..c1100b1
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TestUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.os.Build;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConstantsShim;
+
+/**
+ * Utils class to provide common shared test helper methods or constants that behave differently
+ * depending on the SDK against which they are compiled.
+ */
+public class TestUtils {
+ /**
+ * Whether to test S+ APIs. This requires a) that the test be running on an S+ device, and
+ * b) that the code be compiled against shims new enough to access these APIs.
+ */
+ public static boolean shouldTestSApis() {
+ return SdkLevel.isAtLeastS() && ConstantsShim.VERSION > Build.VERSION_CODES.R;
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TheaterModeTest.java b/tests/cts/net/src/android/net/cts/TheaterModeTest.java
new file mode 100644
index 0000000..d1ddeaa
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TheaterModeTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 android.net.cts;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+public class TheaterModeTest extends AndroidTestCase {
+ private static final String TAG = "TheaterModeTest";
+ private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
+ private static final String FEATURE_WIFI = "android.hardware.wifi";
+ private static final int TIMEOUT_MS = 10 * 1000;
+ private boolean mHasFeature;
+ private Context mContext;
+ private ContentResolver resolver;
+
+ public void setup() {
+ mContext= getContext();
+ resolver = mContext.getContentResolver();
+ mHasFeature = (mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH)
+ || mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI));
+ }
+
+ @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+ public void testTheaterMode() {
+ setup();
+ if (!mHasFeature) {
+ Log.i(TAG, "The device doesn't support network bluetooth or wifi feature");
+ return;
+ }
+
+ for (int testCount = 0; testCount < 2; testCount++) {
+ if (!doOneTest()) {
+ fail("Theater mode failed to change in " + TIMEOUT_MS + "msec");
+ return;
+ }
+ }
+ }
+
+ private boolean doOneTest() {
+ boolean theaterModeOn = isTheaterModeOn();
+
+ setTheaterModeOn(!theaterModeOn);
+ try {
+ Thread.sleep(TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Sleep time interrupted.", e);
+ }
+
+ if (theaterModeOn == isTheaterModeOn()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void setTheaterModeOn(boolean enabling) {
+ // Change the system setting for theater mode
+ Settings.Global.putInt(resolver, Settings.Global.THEATER_MODE_ON, enabling ? 1 : 0);
+ }
+
+ private boolean isTheaterModeOn() {
+ // Read the system setting for theater mode
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.THEATER_MODE_ON, 0) != 0;
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
new file mode 100755
index 0000000..1d9268a
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.NetworkStats;
+import android.net.TrafficStats;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.util.Range;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TrafficStatsTest extends AndroidTestCase {
+ private static final String LOG_TAG = "TrafficStatsTest";
+
+ /** Verify the given value is in range [lower, upper] */
+ private void assertInRange(String tag, long value, long lower, long upper) {
+ final Range range = new Range(lower, upper);
+ assertTrue(tag + ": " + value + " is not within range [" + lower + ", " + upper + "]",
+ range.contains(value));
+ }
+
+ public void testValidMobileStats() {
+ // We can't assume a mobile network is even present in this test, so
+ // we simply assert that a valid value is returned.
+
+ assertTrue(TrafficStats.getMobileTxPackets() >= 0);
+ assertTrue(TrafficStats.getMobileRxPackets() >= 0);
+ assertTrue(TrafficStats.getMobileTxBytes() >= 0);
+ assertTrue(TrafficStats.getMobileRxBytes() >= 0);
+ }
+
+ public void testValidTotalStats() {
+ assertTrue(TrafficStats.getTotalTxPackets() >= 0);
+ assertTrue(TrafficStats.getTotalRxPackets() >= 0);
+ assertTrue(TrafficStats.getTotalTxBytes() >= 0);
+ assertTrue(TrafficStats.getTotalRxBytes() >= 0);
+ }
+
+ public void testValidIfaceStats() {
+ assertTrue(TrafficStats.getTxPackets("lo") >= 0);
+ assertTrue(TrafficStats.getRxPackets("lo") >= 0);
+ assertTrue(TrafficStats.getTxBytes("lo") >= 0);
+ assertTrue(TrafficStats.getRxBytes("lo") >= 0);
+ }
+
+ public void testThreadStatsTag() throws Exception {
+ TrafficStats.setThreadStatsTag(0xf00d);
+ assertTrue("Tag didn't stick", TrafficStats.getThreadStatsTag() == 0xf00d);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread("TrafficStatsTest.testThreadStatsTag") {
+ @Override
+ public void run() {
+ assertTrue("Tag leaked", TrafficStats.getThreadStatsTag() != 0xf00d);
+ TrafficStats.setThreadStatsTag(0xcafe);
+ assertTrue("Tag didn't stick", TrafficStats.getThreadStatsTag() == 0xcafe);
+ latch.countDown();
+ }
+ }.start();
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertTrue("Tag lost", TrafficStats.getThreadStatsTag() == 0xf00d);
+
+ TrafficStats.clearThreadStatsTag();
+ assertTrue("Tag not cleared", TrafficStats.getThreadStatsTag() != 0xf00d);
+ }
+
+ long tcpPacketToIpBytes(long packetCount, long bytes) {
+ // ip header + tcp header + data.
+ // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care.
+ return packetCount * (20 + 32 + bytes);
+ }
+
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testTrafficStatsForLocalhost() throws IOException {
+ final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets();
+ final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets();
+ final long mobileTxBytesBefore = TrafficStats.getMobileTxBytes();
+ final long mobileRxBytesBefore = TrafficStats.getMobileRxBytes();
+ final long totalTxPacketsBefore = TrafficStats.getTotalTxPackets();
+ final long totalRxPacketsBefore = TrafficStats.getTotalRxPackets();
+ final long totalTxBytesBefore = TrafficStats.getTotalTxBytes();
+ final long totalRxBytesBefore = TrafficStats.getTotalRxBytes();
+ final long uidTxBytesBefore = TrafficStats.getUidTxBytes(Process.myUid());
+ final long uidRxBytesBefore = TrafficStats.getUidRxBytes(Process.myUid());
+ final long uidTxPacketsBefore = TrafficStats.getUidTxPackets(Process.myUid());
+ final long uidRxPacketsBefore = TrafficStats.getUidRxPackets(Process.myUid());
+ final long ifaceTxPacketsBefore = TrafficStats.getTxPackets("lo");
+ final long ifaceRxPacketsBefore = TrafficStats.getRxPackets("lo");
+ final long ifaceTxBytesBefore = TrafficStats.getTxBytes("lo");
+ final long ifaceRxBytesBefore = TrafficStats.getRxBytes("lo");
+
+ // Transfer 1MB of data across an explicitly localhost socket.
+ final int byteCount = 1024;
+ final int packetCount = 1024;
+
+ TrafficStats.startDataProfiling(null);
+ final ServerSocket server = new ServerSocket(0);
+ new Thread("TrafficStatsTest.testTrafficStatsForLocalhost") {
+ @Override
+ public void run() {
+ try {
+ final Socket socket = new Socket("localhost", server.getLocalPort());
+ // Make sure that each write()+flush() turns into a packet:
+ // disable Nagle.
+ socket.setTcpNoDelay(true);
+ final OutputStream out = socket.getOutputStream();
+ final byte[] buf = new byte[byteCount];
+ TrafficStats.setThreadStatsTag(0x42);
+ TrafficStats.tagSocket(socket);
+ for (int i = 0; i < packetCount; i++) {
+ out.write(buf);
+ out.flush();
+ try {
+ // Bug: 10668088, Even with Nagle disabled, and flushing the 1024 bytes
+ // the kernel still regroups data into a larger packet.
+ Thread.sleep(5);
+ } catch (InterruptedException e) {
+ }
+ }
+ out.close();
+ socket.close();
+ } catch (IOException e) {
+ Log.i(LOG_TAG, "Badness during writes to socket: " + e);
+ }
+ }
+ }.start();
+
+ int read = 0;
+ try {
+ final Socket socket = server.accept();
+ socket.setTcpNoDelay(true);
+ TrafficStats.setThreadStatsTag(0x43);
+ TrafficStats.tagSocket(socket);
+ final InputStream in = socket.getInputStream();
+ final byte[] buf = new byte[byteCount];
+ while (read < byteCount * packetCount) {
+ int n = in.read(buf);
+ assertTrue("Unexpected EOF", n > 0);
+ read += n;
+ }
+ } finally {
+ server.close();
+ }
+ assertTrue("Not all data read back", read >= byteCount * packetCount);
+
+ // It's too fast to call getUidTxBytes function.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ final NetworkStats testStats = TrafficStats.stopDataProfiling(null);
+
+ final long mobileTxPacketsAfter = TrafficStats.getMobileTxPackets();
+ final long mobileRxPacketsAfter = TrafficStats.getMobileRxPackets();
+ final long mobileTxBytesAfter = TrafficStats.getMobileTxBytes();
+ final long mobileRxBytesAfter = TrafficStats.getMobileRxBytes();
+ final long totalTxPacketsAfter = TrafficStats.getTotalTxPackets();
+ final long totalRxPacketsAfter = TrafficStats.getTotalRxPackets();
+ final long totalTxBytesAfter = TrafficStats.getTotalTxBytes();
+ final long totalRxBytesAfter = TrafficStats.getTotalRxBytes();
+ final long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
+ final long uidRxBytesAfter = TrafficStats.getUidRxBytes(Process.myUid());
+ final long uidTxPacketsAfter = TrafficStats.getUidTxPackets(Process.myUid());
+ final long uidRxPacketsAfter = TrafficStats.getUidRxPackets(Process.myUid());
+ final long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
+ final long uidTxDeltaPackets = uidTxPacketsAfter - uidTxPacketsBefore;
+ final long uidRxDeltaBytes = uidRxBytesAfter - uidRxBytesBefore;
+ final long uidRxDeltaPackets = uidRxPacketsAfter - uidRxPacketsBefore;
+ final long ifaceTxPacketsAfter = TrafficStats.getTxPackets("lo");
+ final long ifaceRxPacketsAfter = TrafficStats.getRxPackets("lo");
+ final long ifaceTxBytesAfter = TrafficStats.getTxBytes("lo");
+ final long ifaceRxBytesAfter = TrafficStats.getRxBytes("lo");
+ final long ifaceTxDeltaPackets = ifaceTxPacketsAfter - ifaceTxPacketsBefore;
+ final long ifaceRxDeltaPackets = ifaceRxPacketsAfter - ifaceRxPacketsBefore;
+ final long ifaceTxDeltaBytes = ifaceTxBytesAfter - ifaceTxBytesBefore;
+ final long ifaceRxDeltaBytes = ifaceRxBytesAfter - ifaceRxBytesBefore;
+
+ // Localhost traffic *does* count against per-UID stats.
+ /*
+ * Calculations:
+ * - bytes
+ * bytes is approx: packets * data + packets * acks;
+ * but sometimes there are less acks than packets, so we set a lower
+ * limit of 1 ack.
+ * - setup/teardown
+ * + 7 approx.: syn, syn-ack, ack, fin-ack, ack, fin-ack, ack;
+ * but sometimes the last find-acks just vanish, so we set a lower limit of +5.
+ */
+ final int maxExpectedExtraPackets = 7;
+ final int minExpectedExtraPackets = 5;
+
+ // Some other tests don't cleanup connections correctly.
+ // They have the same UID, so we discount their lingering traffic
+ // which happens only on non-localhost, such as TCP FIN retranmission packets
+ final long deltaTxOtherPackets = (totalTxPacketsAfter - totalTxPacketsBefore)
+ - uidTxDeltaPackets;
+ final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore)
+ - uidRxDeltaPackets;
+ if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) {
+ Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/"
+ + deltaRxOtherPackets);
+ }
+
+ // Check that the per-uid stats obtained from data profiling contain the expected values.
+ // The data profiling snapshot is generated from the readNetworkStatsDetail() method in
+ // networkStatsService, so it's possible to verify that the detailed stats for a given
+ // uid are correct.
+ final NetworkStats.Entry entry = testStats.getTotal(null, Process.myUid());
+ final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount);
+ final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0);
+ final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0);
+ final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0);
+ final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0);
+ final long deltaRxOtherPktBytes = tcpPacketToIpBytes(deltaRxOtherPackets, 0);
+ assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets,
+ uidTxDeltaPackets);
+ assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets,
+ uidRxDeltaPackets);
+ assertInRange("txBytes detail", entry.txBytes, pktBytes + minExpExtraPktBytes,
+ uidTxDeltaBytes);
+ assertInRange("rxBytes detail", entry.rxBytes, pktBytes + minExpExtraPktBytes,
+ uidRxDeltaBytes);
+ assertInRange("uidtxp", uidTxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
+ assertInRange("uidrxp", uidRxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
+ assertInRange("uidtxb", uidTxDeltaBytes, pktBytes + minExpExtraPktBytes,
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes);
+ assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes,
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
+ assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets);
+ assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets);
+ assertInRange("iftxb", ifaceTxDeltaBytes, pktBytes + minExpExtraPktBytes,
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ assertInRange("ifrxb", ifaceRxDeltaBytes, pktBytes + minExpExtraPktBytes,
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+
+ // Localhost traffic *does* count against total stats.
+ // Check the total stats increased after test data transfer over localhost has been made.
+ assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter,
+ totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets);
+ assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter,
+ totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets);
+ assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter,
+ totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes);
+ assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter,
+ totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes);
+ assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter,
+ totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets);
+ assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter,
+ totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets);
+ assertTrue("iftxb: " + ifaceTxBytesBefore + " -> " + ifaceTxBytesAfter,
+ totalTxBytesAfter >= totalTxBytesBefore + ifaceTxDeltaBytes);
+ assertTrue("ifrxb: " + ifaceRxBytesBefore + " -> " + ifaceRxBytesAfter,
+ totalRxBytesAfter >= totalRxBytesBefore + ifaceRxDeltaBytes);
+
+ // Localhost traffic should *not* count against mobile stats,
+ // There might be some other traffic, but nowhere near 1MB.
+ assertInRange("mtxp", mobileTxPacketsAfter, mobileTxPacketsBefore,
+ mobileTxPacketsBefore + 500);
+ assertInRange("mrxp", mobileRxPacketsAfter, mobileRxPacketsBefore,
+ mobileRxPacketsBefore + 500);
+ assertInRange("mtxb", mobileTxBytesAfter, mobileTxBytesBefore,
+ mobileTxBytesBefore + 200000);
+ assertInRange("mrxb", mobileRxBytesAfter, mobileRxBytesBefore,
+ mobileRxBytesBefore + 200000);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
new file mode 100644
index 0000000..7887385
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.IPPROTO_ESP;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class TunUtils {
+ private static final String TAG = TunUtils.class.getSimpleName();
+
+ protected static final int IP4_ADDR_OFFSET = 12;
+ protected static final int IP4_ADDR_LEN = 4;
+ protected static final int IP6_ADDR_OFFSET = 8;
+ protected static final int IP6_ADDR_LEN = 16;
+ protected static final int IP4_PROTO_OFFSET = 9;
+ protected static final int IP6_PROTO_OFFSET = 6;
+
+ private static final int DATA_BUFFER_LEN = 4096;
+ private static final int TIMEOUT = 2000;
+
+ private final List<byte[]> mPackets = new ArrayList<>();
+ private final ParcelFileDescriptor mTunFd;
+ private final Thread mReaderThread;
+
+ public TunUtils(ParcelFileDescriptor tunFd) {
+ mTunFd = tunFd;
+
+ // Start background reader thread
+ mReaderThread =
+ new Thread(
+ () -> {
+ try {
+ // Loop will exit and thread will quit when tunFd is closed.
+ // Receiving either EOF or an exception will exit this reader loop.
+ // FileInputStream in uninterruptable, so there's no good way to
+ // ensure that this thread shuts down except upon FD closure.
+ while (true) {
+ byte[] intercepted = receiveFromTun();
+ if (intercepted == null) {
+ // Exit once we've hit EOF
+ return;
+ } else if (intercepted.length > 0) {
+ // Only save packet if we've received any bytes.
+ synchronized (mPackets) {
+ mPackets.add(intercepted);
+ mPackets.notifyAll();
+ }
+ }
+ }
+ } catch (IOException ignored) {
+ // Simply exit this reader thread
+ return;
+ }
+ });
+ mReaderThread.start();
+ }
+
+ private byte[] receiveFromTun() throws IOException {
+ FileInputStream in = new FileInputStream(mTunFd.getFileDescriptor());
+ byte[] inBytes = new byte[DATA_BUFFER_LEN];
+ int bytesRead = in.read(inBytes);
+
+ if (bytesRead < 0) {
+ return null; // return null for EOF
+ } else if (bytesRead >= DATA_BUFFER_LEN) {
+ throw new IllegalStateException("Too big packet. Fragmentation unsupported");
+ }
+ return Arrays.copyOf(inBytes, bytesRead);
+ }
+
+ private byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
+ synchronized (mPackets) {
+ for (int i = startIndex; i < mPackets.size(); i++) {
+ byte[] pkt = mPackets.get(i);
+ if (verifier.test(pkt)) {
+ return pkt;
+ }
+ }
+ }
+ return null;
+ }
+
+ protected byte[] awaitPacket(Predicate<byte[]> verifier) throws Exception {
+ long endTime = System.currentTimeMillis() + TIMEOUT;
+ int startIndex = 0;
+
+ synchronized (mPackets) {
+ while (System.currentTimeMillis() < endTime) {
+ final byte[] pkt = getFirstMatchingPacket(verifier, startIndex);
+ if (pkt != null) {
+ return pkt; // We've found the packet we're looking for.
+ }
+
+ startIndex = mPackets.size();
+
+ // Try to prevent waiting too long. If waitTimeout <= 0, we've already hit timeout
+ long waitTimeout = endTime - System.currentTimeMillis();
+ if (waitTimeout > 0) {
+ mPackets.wait(waitTimeout);
+ }
+ }
+ }
+
+ fail("No packet found matching verifier");
+ throw new IllegalStateException("Impossible condition; should have thrown in fail()");
+ }
+
+ public byte[] awaitEspPacketNoPlaintext(
+ int spi, byte[] plaintext, boolean useEncap, int expectedPacketSize) throws Exception {
+ final byte[] espPkt = awaitPacket(
+ (pkt) -> isEspFailIfSpecifiedPlaintextFound(pkt, spi, useEncap, plaintext));
+
+ // Validate packet size
+ assertEquals(expectedPacketSize, espPkt.length);
+
+ return espPkt; // We've found the packet we're looking for.
+ }
+
+ private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+ // Check SPI byte by byte.
+ return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff)
+ && pkt[espOffset + 1] == (byte) ((spi >>> 16) & 0xff)
+ && pkt[espOffset + 2] == (byte) ((spi >>> 8) & 0xff)
+ && pkt[espOffset + 3] == (byte) (spi & 0xff);
+ }
+
+ /**
+ * Variant of isEsp that also fails the test if the provided plaintext is found
+ *
+ * @param pkt the packet bytes to verify
+ * @param spi the expected SPI to look for
+ * @param encap whether encap was enabled, and the packet has a UDP header
+ * @param plaintext the plaintext packet before outbound encryption, which MUST not appear in
+ * the provided packet.
+ */
+ private static boolean isEspFailIfSpecifiedPlaintextFound(
+ byte[] pkt, int spi, boolean encap, byte[] plaintext) {
+ if (Collections.indexOfSubList(Arrays.asList(pkt), Arrays.asList(plaintext)) != -1) {
+ fail("Banned plaintext packet found");
+ }
+
+ return isEsp(pkt, spi, encap);
+ }
+
+ private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+ if (isIpv6(pkt)) {
+ // IPv6 UDP encap not supported by kernels; assume non-encap.
+ return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+ } 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);
+ } else {
+ return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+ }
+ }
+ }
+
+ public static boolean isIpv6(byte[] pkt) {
+ // First nibble shows IP version. 0x60 for IPv6
+ return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
+ }
+
+ private static byte[] getReflectedPacket(byte[] pkt) {
+ byte[] reflected = Arrays.copyOf(pkt, pkt.length);
+
+ if (isIpv6(pkt)) {
+ // Set reflected packet's dst to that of the original's src
+ System.arraycopy(
+ pkt, // src
+ IP6_ADDR_OFFSET + IP6_ADDR_LEN, // src offset
+ reflected, // dst
+ IP6_ADDR_OFFSET, // dst offset
+ IP6_ADDR_LEN); // len
+ // Set reflected packet's src IP to that of the original's dst IP
+ System.arraycopy(
+ pkt, // src
+ IP6_ADDR_OFFSET, // src offset
+ reflected, // dst
+ IP6_ADDR_OFFSET + IP6_ADDR_LEN, // dst offset
+ IP6_ADDR_LEN); // len
+ } else {
+ // Set reflected packet's dst to that of the original's src
+ System.arraycopy(
+ pkt, // src
+ IP4_ADDR_OFFSET + IP4_ADDR_LEN, // src offset
+ reflected, // dst
+ IP4_ADDR_OFFSET, // dst offset
+ IP4_ADDR_LEN); // len
+ // Set reflected packet's src IP to that of the original's dst IP
+ System.arraycopy(
+ pkt, // src
+ IP4_ADDR_OFFSET, // src offset
+ reflected, // dst
+ IP4_ADDR_OFFSET + IP4_ADDR_LEN, // dst offset
+ IP4_ADDR_LEN); // len
+ }
+ return reflected;
+ }
+
+ /** Takes all captured packets, flips the src/dst, and re-injects them. */
+ public void reflectPackets() throws IOException {
+ synchronized (mPackets) {
+ for (byte[] pkt : mPackets) {
+ injectPacket(getReflectedPacket(pkt));
+ }
+ }
+ }
+
+ public void injectPacket(byte[] pkt) throws IOException {
+ FileOutputStream out = new FileOutputStream(mTunFd.getFileDescriptor());
+ out.write(pkt);
+ out.flush();
+ }
+
+ /** Resets the intercepted packets. */
+ public void reset() throws IOException {
+ synchronized (mPackets) {
+ mPackets.clear();
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UriTest.java b/tests/cts/net/src/android/net/cts/UriTest.java
new file mode 100644
index 0000000..40b8fb7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UriTest.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.content.ContentUris;
+import android.net.Uri;
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+import java.io.File;
+import java.util.Arrays;
+import java.util.ArrayList;
+
+public class UriTest extends AndroidTestCase {
+ public void testParcelling() {
+ parcelAndUnparcel(Uri.parse("foo:bob%20lee"));
+ parcelAndUnparcel(Uri.fromParts("foo", "bob lee", "fragment"));
+ parcelAndUnparcel(new Uri.Builder()
+ .scheme("http")
+ .authority("crazybob.org")
+ .path("/rss/")
+ .encodedQuery("a=b")
+ .fragment("foo")
+ .build());
+ }
+
+ private void parcelAndUnparcel(Uri u) {
+ Parcel p = Parcel.obtain();
+ Uri.writeToParcel(p, u);
+ p.setDataPosition(0);
+ assertEquals(u, Uri.CREATOR.createFromParcel(p));
+
+ p.setDataPosition(0);
+ u = u.buildUpon().build();
+ Uri.writeToParcel(p, u);
+ p.setDataPosition(0);
+ assertEquals(u, Uri.CREATOR.createFromParcel(p));
+ }
+
+ public void testBuildUpon() {
+ Uri u = Uri.parse("bob:lee").buildUpon().scheme("robert").build();
+ assertEquals("robert", u.getScheme());
+ assertEquals("lee", u.getEncodedSchemeSpecificPart());
+ assertEquals("lee", u.getSchemeSpecificPart());
+ assertNull(u.getQuery());
+ assertNull(u.getPath());
+ assertNull(u.getAuthority());
+ assertNull(u.getHost());
+
+ Uri a = Uri.fromParts("foo", "bar", "tee");
+ Uri b = a.buildUpon().fragment("new").build();
+ assertEquals("new", b.getFragment());
+ assertEquals("bar", b.getSchemeSpecificPart());
+ assertEquals("foo", b.getScheme());
+ a = new Uri.Builder()
+ .scheme("foo")
+ .encodedOpaquePart("bar")
+ .fragment("tee")
+ .build();
+ b = a.buildUpon().fragment("new").build();
+ assertEquals("new", b.getFragment());
+ assertEquals("bar", b.getSchemeSpecificPart());
+ assertEquals("foo", b.getScheme());
+
+ a = Uri.fromParts("scheme", "[2001:db8::dead:e1f]/foo", "bar");
+ b = a.buildUpon().fragment("qux").build();
+ assertEquals("qux", b.getFragment());
+ assertEquals("[2001:db8::dead:e1f]/foo", b.getSchemeSpecificPart());
+ assertEquals("scheme", b.getScheme());
+ }
+
+ public void testStringUri() {
+ assertEquals("bob lee",
+ Uri.parse("foo:bob%20lee").getSchemeSpecificPart());
+ assertEquals("bob%20lee",
+ Uri.parse("foo:bob%20lee").getEncodedSchemeSpecificPart());
+
+ assertEquals("/bob%20lee",
+ Uri.parse("foo:/bob%20lee").getEncodedPath());
+ assertNull(Uri.parse("foo:bob%20lee").getPath());
+
+ assertEquals("bob%20lee",
+ Uri.parse("foo:?bob%20lee").getEncodedQuery());
+ assertNull(Uri.parse("foo:bob%20lee").getEncodedQuery());
+ assertNull(Uri.parse("foo:bar#?bob%20lee").getQuery());
+
+ assertEquals("bob%20lee",
+ Uri.parse("foo:#bob%20lee").getEncodedFragment());
+
+ Uri uri = Uri.parse("http://localhost:42");
+ assertEquals("localhost", uri.getHost());
+ assertEquals(42, uri.getPort());
+
+ uri = Uri.parse("http://bob@localhost:42");
+ assertEquals("bob", uri.getUserInfo());
+ assertEquals("localhost", uri.getHost());
+ assertEquals(42, uri.getPort());
+
+ uri = Uri.parse("http://bob%20lee@localhost:42");
+ assertEquals("bob lee", uri.getUserInfo());
+ assertEquals("bob%20lee", uri.getEncodedUserInfo());
+
+ uri = Uri.parse("http://localhost");
+ assertEquals("localhost", uri.getHost());
+ assertEquals(-1, uri.getPort());
+
+ uri = Uri.parse("http://a:a@example.com:a@example2.com/path");
+ assertEquals("a:a@example.com:a@example2.com", uri.getAuthority());
+ assertEquals("example2.com", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("/path", uri.getPath());
+
+ uri = Uri.parse("http://a.foo.com\\.example.com/path");
+ assertEquals("a.foo.com", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("\\.example.com/path", uri.getPath());
+
+ uri = Uri.parse("https://[2001:db8::dead:e1f]/foo");
+ assertEquals("[2001:db8::dead:e1f]", uri.getAuthority());
+ assertNull(uri.getUserInfo());
+ assertEquals("[2001:db8::dead:e1f]", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("/foo", uri.getPath());
+ assertEquals(null, uri.getFragment());
+ assertEquals("//[2001:db8::dead:e1f]/foo", uri.getSchemeSpecificPart());
+
+ uri = Uri.parse("https://[2001:db8::dead:e1f]/#foo");
+ assertEquals("[2001:db8::dead:e1f]", uri.getAuthority());
+ assertNull(uri.getUserInfo());
+ assertEquals("[2001:db8::dead:e1f]", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("/", uri.getPath());
+ assertEquals("foo", uri.getFragment());
+ assertEquals("//[2001:db8::dead:e1f]/", uri.getSchemeSpecificPart());
+
+ uri = Uri.parse(
+ "https://some:user@[2001:db8::dead:e1f]:1234/foo?corge=thud&corge=garp#bar");
+ assertEquals("some:user@[2001:db8::dead:e1f]:1234", uri.getAuthority());
+ assertEquals("some:user", uri.getUserInfo());
+ assertEquals("[2001:db8::dead:e1f]", uri.getHost());
+ assertEquals(1234, uri.getPort());
+ assertEquals("/foo", uri.getPath());
+ assertEquals("bar", uri.getFragment());
+ assertEquals("//some:user@[2001:db8::dead:e1f]:1234/foo?corge=thud&corge=garp",
+ uri.getSchemeSpecificPart());
+ assertEquals("corge=thud&corge=garp", uri.getQuery());
+ assertEquals("thud", uri.getQueryParameter("corge"));
+ assertEquals(Arrays.asList("thud", "garp"), uri.getQueryParameters("corge"));
+ }
+
+ public void testCompareTo() {
+ Uri a = Uri.parse("foo:a");
+ Uri b = Uri.parse("foo:b");
+ Uri b2 = Uri.parse("foo:b");
+
+ assertTrue(a.compareTo(b) < 0);
+ assertTrue(b.compareTo(a) > 0);
+ assertEquals(0, b.compareTo(b2));
+ }
+
+ public void testEqualsAndHashCode() {
+ Uri a = Uri.parse("http://crazybob.org/test/?foo=bar#tee");
+
+ Uri b = new Uri.Builder()
+ .scheme("http")
+ .authority("crazybob.org")
+ .path("/test/")
+ .encodedQuery("foo=bar")
+ .fragment("tee")
+ .build();
+
+ // Try alternate builder methods.
+ Uri c = new Uri.Builder()
+ .scheme("http")
+ .encodedAuthority("crazybob.org")
+ .encodedPath("/test/")
+ .encodedQuery("foo=bar")
+ .encodedFragment("tee")
+ .build();
+
+ assertFalse(Uri.EMPTY.equals(null));
+ assertEquals(a, b);
+ assertEquals(b, c);
+ assertEquals(c, a);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertEquals(b.hashCode(), c.hashCode());
+ }
+
+ public void testEncodeAndDecode() {
+ String encoded = Uri.encode("Bob:/", "/");
+ assertEquals(-1, encoded.indexOf(':'));
+ assertTrue(encoded.indexOf('/') > -1);
+ assertEncodeDecodeRoundtripExact(null);
+ assertEncodeDecodeRoundtripExact("");
+ assertEncodeDecodeRoundtripExact("Bob");
+ assertEncodeDecodeRoundtripExact(":Bob");
+ assertEncodeDecodeRoundtripExact("::Bob");
+ assertEncodeDecodeRoundtripExact("Bob::Lee");
+ assertEncodeDecodeRoundtripExact("Bob:Lee");
+ assertEncodeDecodeRoundtripExact("Bob::");
+ assertEncodeDecodeRoundtripExact("Bob:");
+ assertEncodeDecodeRoundtripExact("::Bob::");
+ assertEncodeDecodeRoundtripExact("https:/some:user@[2001:db8::dead:e1f]:1234/foo#bar");
+ }
+
+ private static void assertEncodeDecodeRoundtripExact(String s) {
+ assertEquals(s, Uri.decode(Uri.encode(s, null)));
+ }
+
+ public void testDecode_emptyString_returnsEmptyString() {
+ assertEquals("", Uri.decode(""));
+ }
+
+ public void testDecode_null_returnsNull() {
+ assertNull(Uri.decode(null));
+ }
+
+ public void testDecode_wrongHexDigit() {
+ // %p in the end.
+ assertEquals("ab/$\u0102%\u0840\uFFFD\u0000", Uri.decode("ab%2f$%C4%82%25%e0%a1%80%p"));
+ }
+
+ public void testDecode_secondHexDigitWrong() {
+ // %1p in the end.
+ assertEquals("ab/$\u0102%\u0840\uFFFD\u0001", Uri.decode("ab%2f$%c4%82%25%e0%a1%80%1p"));
+ }
+
+ public void testDecode_endsWithPercent_appendsUnknownCharacter() {
+ // % in the end.
+ assertEquals("ab/$\u0102%\u0840\uFFFD", Uri.decode("ab%2f$%c4%82%25%e0%a1%80%"));
+ }
+
+ public void testDecode_plusNotConverted() {
+ assertEquals("ab/$\u0102%+\u0840", Uri.decode("ab%2f$%c4%82%25+%e0%a1%80"));
+ }
+
+ // Last character needs decoding (make sure we are flushing the buffer with chars to decode).
+ public void testDecode_lastCharacter() {
+ assertEquals("ab/$\u0102%\u0840", Uri.decode("ab%2f$%c4%82%25%e0%a1%80"));
+ }
+
+ // Check that a second row of encoded characters is decoded properly (internal buffers are
+ // reset properly).
+ public void testDecode_secondRowOfEncoded() {
+ assertEquals("ab/$\u0102%\u0840aa\u0840",
+ Uri.decode("ab%2f$%c4%82%25%e0%a1%80aa%e0%a1%80"));
+ }
+
+ public void testFromFile() {
+ File f = new File("/tmp/bob");
+ Uri uri = Uri.fromFile(f);
+ assertEquals("file:///tmp/bob", uri.toString());
+ try {
+ Uri.fromFile(null);
+ fail("testFile fail");
+ } catch (NullPointerException e) {}
+ }
+
+ public void testQueryParameters() {
+ Uri uri = Uri.parse("content://user");
+ assertEquals(null, uri.getQueryParameter("a"));
+
+ uri = uri.buildUpon().appendQueryParameter("a", "b").build();
+ assertEquals("b", uri.getQueryParameter("a"));
+
+ uri = uri.buildUpon().appendQueryParameter("a", "b2").build();
+ assertEquals(Arrays.asList("b", "b2"), uri.getQueryParameters("a"));
+
+ uri = uri.buildUpon().appendQueryParameter("c", "d").build();
+ assertEquals(Arrays.asList("b", "b2"), uri.getQueryParameters("a"));
+ assertEquals("d", uri.getQueryParameter("c"));
+ }
+
+ public void testPathOperations() {
+ Uri uri = Uri.parse("content://user/a/b");
+
+ assertEquals(2, uri.getPathSegments().size());
+ assertEquals("a", uri.getPathSegments().get(0));
+ assertEquals("b", uri.getPathSegments().get(1));
+ assertEquals("b", uri.getLastPathSegment());
+
+ Uri first = uri;
+ uri = uri.buildUpon().appendPath("c").build();
+ assertEquals(3, uri.getPathSegments().size());
+ assertEquals("c", uri.getPathSegments().get(2));
+ assertEquals("c", uri.getLastPathSegment());
+ assertEquals("content://user/a/b/c", uri.toString());
+
+ uri = ContentUris.withAppendedId(uri, 100);
+ assertEquals(4, uri.getPathSegments().size());
+ assertEquals("100", uri.getPathSegments().get(3));
+ assertEquals("100", uri.getLastPathSegment());
+ assertEquals(100, ContentUris.parseId(uri));
+ assertEquals("content://user/a/b/c/100", uri.toString());
+
+ // Make sure the original URI is still intact.
+ assertEquals(2, first.getPathSegments().size());
+ assertEquals("b", first.getLastPathSegment());
+
+ try {
+ first.getPathSegments().get(2);
+ fail("test path operations");
+ } catch (IndexOutOfBoundsException e) {}
+
+ assertEquals(null, Uri.EMPTY.getLastPathSegment());
+
+ Uri withC = Uri.parse("foo:/a/b/").buildUpon().appendPath("c").build();
+ assertEquals("/a/b/c", withC.getPath());
+ }
+
+ public void testOpaqueUri() {
+ Uri uri = Uri.parse("mailto:nobody");
+ testOpaqueUri(uri);
+
+ uri = uri.buildUpon().build();
+ testOpaqueUri(uri);
+
+ uri = Uri.fromParts("mailto", "nobody", null);
+ testOpaqueUri(uri);
+
+ uri = uri.buildUpon().build();
+ testOpaqueUri(uri);
+
+ uri = new Uri.Builder()
+ .scheme("mailto")
+ .opaquePart("nobody")
+ .build();
+ testOpaqueUri(uri);
+
+ uri = uri.buildUpon().build();
+ testOpaqueUri(uri);
+ }
+
+ private void testOpaqueUri(Uri uri) {
+ assertEquals("mailto", uri.getScheme());
+ assertEquals("nobody", uri.getSchemeSpecificPart());
+ assertEquals("nobody", uri.getEncodedSchemeSpecificPart());
+
+ assertNull(uri.getFragment());
+ assertTrue(uri.isAbsolute());
+ assertTrue(uri.isOpaque());
+ assertFalse(uri.isRelative());
+ assertFalse(uri.isHierarchical());
+
+ assertNull(uri.getAuthority());
+ assertNull(uri.getEncodedAuthority());
+ assertNull(uri.getPath());
+ assertNull(uri.getEncodedPath());
+ assertNull(uri.getUserInfo());
+ assertNull(uri.getEncodedUserInfo());
+ assertNull(uri.getQuery());
+ assertNull(uri.getEncodedQuery());
+ assertNull(uri.getHost());
+ assertEquals(-1, uri.getPort());
+
+ assertTrue(uri.getPathSegments().isEmpty());
+ assertNull(uri.getLastPathSegment());
+
+ assertEquals("mailto:nobody", uri.toString());
+
+ Uri withFragment = uri.buildUpon().fragment("top").build();
+ assertEquals("mailto:nobody#top", withFragment.toString());
+ }
+
+ public void testHierarchicalUris() {
+ testHierarchical("http", "google.com", "/p1/p2", "query", "fragment");
+ testHierarchical("file", null, "/p1/p2", null, null);
+ testHierarchical("content", "contact", "/p1/p2", null, null);
+ testHierarchical("http", "google.com", "/p1/p2", null, "fragment");
+ testHierarchical("http", "google.com", "", null, "fragment");
+ testHierarchical("http", "google.com", "", "query", "fragment");
+ testHierarchical("http", "google.com", "", "query", null);
+ testHierarchical("http", null, "/", "query", null);
+ }
+
+ private static void testHierarchical(String scheme, String authority,
+ String path, String query, String fragment) {
+ StringBuilder sb = new StringBuilder();
+
+ if (authority != null) {
+ sb.append("//").append(authority);
+ }
+ if (path != null) {
+ sb.append(path);
+ }
+ if (query != null) {
+ sb.append('?').append(query);
+ }
+
+ String ssp = sb.toString();
+
+ if (scheme != null) {
+ sb.insert(0, scheme + ":");
+ }
+ if (fragment != null) {
+ sb.append('#').append(fragment);
+ }
+
+ String uriString = sb.toString();
+
+ Uri uri = Uri.parse(uriString);
+
+ // Run these twice to test caching.
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+
+ // Test rebuilt version.
+ uri = uri.buildUpon().build();
+
+ // Run these twice to test caching.
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+
+ // The decoded and encoded versions of the inputs are all the same.
+ // We'll test the actual encoding decoding separately.
+
+ // Test building with encoded versions.
+ Uri built = new Uri.Builder()
+ .scheme(scheme)
+ .encodedAuthority(authority)
+ .encodedPath(path)
+ .encodedQuery(query)
+ .encodedFragment(fragment)
+ .build();
+
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+
+ // Test building with decoded versions.
+ built = new Uri.Builder()
+ .scheme(scheme)
+ .authority(authority)
+ .path(path)
+ .query(query)
+ .fragment(fragment)
+ .build();
+
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+
+ // Rebuild.
+ built = built.buildUpon().build();
+
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ }
+
+ private static void compareHierarchical(String uriString, String ssp,
+ Uri uri,
+ String scheme, String authority, String path, String query,
+ String fragment) {
+ assertEquals(scheme, uri.getScheme());
+ assertEquals(authority, uri.getAuthority());
+ assertEquals(authority, uri.getEncodedAuthority());
+ assertEquals(path, uri.getPath());
+ assertEquals(path, uri.getEncodedPath());
+ assertEquals(query, uri.getQuery());
+ assertEquals(query, uri.getEncodedQuery());
+ assertEquals(fragment, uri.getFragment());
+ assertEquals(fragment, uri.getEncodedFragment());
+ assertEquals(ssp, uri.getSchemeSpecificPart());
+
+ if (scheme != null) {
+ assertTrue(uri.isAbsolute());
+ assertFalse(uri.isRelative());
+ } else {
+ assertFalse(uri.isAbsolute());
+ assertTrue(uri.isRelative());
+ }
+
+ assertFalse(uri.isOpaque());
+ assertTrue(uri.isHierarchical());
+ assertEquals(uriString, uri.toString());
+ }
+
+ public void testNormalizeScheme() {
+ assertEquals(Uri.parse(""), Uri.parse("").normalizeScheme());
+ assertEquals(Uri.parse("http://www.android.com"),
+ Uri.parse("http://www.android.com").normalizeScheme());
+ assertEquals(Uri.parse("http://USER@WWW.ANDROID.COM:100/ABOUT?foo=blah@bar=bleh#c"),
+ Uri.parse("HTTP://USER@WWW.ANDROID.COM:100/ABOUT?foo=blah@bar=bleh#c")
+ .normalizeScheme());
+ }
+
+ public void testToSafeString_tel() {
+ checkToSafeString("tel:xxxxxx", "tel:Google");
+ checkToSafeString("tel:xxxxxxxxxx", "tel:1234567890");
+ checkToSafeString("tEl:xxx.xxx-xxxx", "tEl:123.456-7890");
+ }
+
+ public void testToSafeString_sip() {
+ checkToSafeString("sip:xxxxxxx@xxxxxxx.xxxxxxxx", "sip:android@android.com:1234");
+ checkToSafeString("sIp:xxxxxxx@xxxxxxx.xxx", "sIp:android@android.com");
+ }
+
+ public void testToSafeString_sms() {
+ checkToSafeString("sms:xxxxxx", "sms:123abc");
+ checkToSafeString("smS:xxx.xxx-xxxx", "smS:123.456-7890");
+ }
+
+ public void testToSafeString_smsto() {
+ checkToSafeString("smsto:xxxxxx", "smsto:123abc");
+ checkToSafeString("SMSTo:xxx.xxx-xxxx", "SMSTo:123.456-7890");
+ }
+
+ public void testToSafeString_mailto() {
+ checkToSafeString("mailto:xxxxxxx@xxxxxxx.xxx", "mailto:android@android.com");
+ checkToSafeString("Mailto:xxxxxxx@xxxxxxx.xxxxxxxxxx",
+ "Mailto:android@android.com/secret");
+ }
+
+ public void testToSafeString_nfc() {
+ checkToSafeString("nfc:xxxxxx", "nfc:123abc");
+ checkToSafeString("nfc:xxx.xxx-xxxx", "nfc:123.456-7890");
+ checkToSafeString("nfc:xxxxxxx@xxxxxxx.xxx", "nfc:android@android.com");
+ }
+
+ public void testToSafeString_http() {
+ checkToSafeString("http://www.android.com/...", "http://www.android.com");
+ checkToSafeString("HTTP://www.android.com/...", "HTTP://www.android.com");
+ checkToSafeString("http://www.android.com/...", "http://www.android.com/");
+ checkToSafeString("http://www.android.com/...", "http://www.android.com/secretUrl?param");
+ checkToSafeString("http://www.android.com/...",
+ "http://user:pwd@www.android.com/secretUrl?param");
+ checkToSafeString("http://www.android.com/...",
+ "http://user@www.android.com/secretUrl?param");
+ checkToSafeString("http://www.android.com/...", "http://www.android.com/secretUrl?param");
+ checkToSafeString("http:///...", "http:///path?param");
+ checkToSafeString("http:///...", "http://");
+ checkToSafeString("http://:12345/...", "http://:12345/");
+ }
+
+ public void testToSafeString_https() {
+ checkToSafeString("https://www.android.com/...", "https://www.android.com/secretUrl?param");
+ checkToSafeString("https://www.android.com:8443/...",
+ "https://user:pwd@www.android.com:8443/secretUrl?param");
+ checkToSafeString("https://www.android.com/...", "https://user:pwd@www.android.com");
+ checkToSafeString("Https://www.android.com/...", "Https://user:pwd@www.android.com");
+ }
+
+ public void testToSafeString_ftp() {
+ checkToSafeString("ftp://ftp.android.com/...", "ftp://ftp.android.com/");
+ checkToSafeString("ftP://ftp.android.com/...", "ftP://anonymous@ftp.android.com/");
+ checkToSafeString("ftp://ftp.android.com:2121/...",
+ "ftp://root:love@ftp.android.com:2121/");
+ }
+
+ public void testToSafeString_rtsp() {
+ checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/");
+ checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/video.mov");
+ checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/video.mov?param");
+ checkToSafeString("RtsP://rtsp.android.com/...", "RtsP://anonymous@rtsp.android.com/");
+ checkToSafeString("rtsp://rtsp.android.com:2121/...",
+ "rtsp://username:password@rtsp.android.com:2121/");
+ }
+
+ public void testToSafeString_notSupport() {
+ checkToSafeString("unsupported://ajkakjah/askdha/secret?secret",
+ "unsupported://ajkakjah/askdha/secret?secret");
+ checkToSafeString("unsupported:ajkakjah/askdha/secret?secret",
+ "unsupported:ajkakjah/askdha/secret?secret");
+ }
+
+ private void checkToSafeString(String expectedSafeString, String original) {
+ assertEquals(expectedSafeString, Uri.parse(original).toSafeString());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java b/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java
new file mode 100644
index 0000000..4088d82
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import junit.framework.TestCase;
+import android.net.Uri.Builder;
+import android.net.Uri;
+
+public class Uri_BuilderTest extends TestCase {
+ public void testBuilderOperations() {
+ Uri uri = Uri.parse("http://google.com/p1?query#fragment");
+ Builder builder = uri.buildUpon();
+ uri = builder.appendPath("p2").build();
+ assertEquals("http", uri.getScheme());
+ assertEquals("google.com", uri.getAuthority());
+ assertEquals("/p1/p2", uri.getPath());
+ assertEquals("query", uri.getQuery());
+ assertEquals("fragment", uri.getFragment());
+ assertEquals(uri.toString(), builder.toString());
+
+ uri = Uri.parse("mailto:nobody");
+ builder = uri.buildUpon();
+ uri = builder.build();
+ assertEquals("mailto", uri.getScheme());
+ assertEquals("nobody", uri.getSchemeSpecificPart());
+ assertEquals(uri.toString(), builder.toString());
+
+ uri = new Uri.Builder()
+ .scheme("http")
+ .encodedAuthority("google.com")
+ .encodedPath("/p1")
+ .appendEncodedPath("p2")
+ .encodedQuery("query")
+ .appendQueryParameter("query2", null)
+ .encodedFragment("fragment")
+ .build();
+ assertEquals("http", uri.getScheme());
+ assertEquals("google.com", uri.getEncodedAuthority());
+ assertEquals("/p1/p2", uri.getEncodedPath());
+ assertEquals("query&query2=null", uri.getEncodedQuery());
+ assertEquals("fragment", uri.getEncodedFragment());
+
+ uri = new Uri.Builder()
+ .scheme("mailto")
+ .encodedOpaquePart("nobody")
+ .build();
+ assertEquals("mailto", uri.getScheme());
+ assertEquals("nobody", uri.getEncodedSchemeSpecificPart());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java
new file mode 100644
index 0000000..5a70928
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.UrlQuerySanitizer;
+import android.net.UrlQuerySanitizer.IllegalCharacterValueSanitizer;
+import android.net.UrlQuerySanitizer.ParameterValuePair;
+import android.net.UrlQuerySanitizer.ValueSanitizer;
+import android.os.Build;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UrlQuerySanitizerTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final int ALL_OK = IllegalCharacterValueSanitizer.ALL_OK;
+
+ // URL for test.
+ private static final String TEST_URL = "http://example.com/?name=Joe+User&age=20&height=175";
+
+ // Default sanitizer's change when "+".
+ private static final String EXPECTED_UNDERLINE_NAME = "Joe_User";
+
+ // IllegalCharacterValueSanitizer sanitizer's change when "+".
+ private static final String EXPECTED_SPACE_NAME = "Joe User";
+ private static final String EXPECTED_AGE = "20";
+ private static final String EXPECTED_HEIGHT = "175";
+ private static final String NAME = "name";
+ private static final String AGE = "age";
+ private static final String HEIGHT = "height";
+
+ @Test
+ public void testUrlQuerySanitizer() {
+ MockUrlQuerySanitizer uqs = new MockUrlQuerySanitizer();
+ assertFalse(uqs.getAllowUnregisteredParamaters());
+
+ final String query = "book=thinking in java&price=108";
+ final String book = "book";
+ final String bookName = "thinking in java";
+ final String price = "price";
+ final String bookPrice = "108";
+ final String notExistPar = "notExistParameter";
+ uqs.registerParameters(new String[]{book, price}, UrlQuerySanitizer.getSpaceLegal());
+ uqs.parseQuery(query);
+ assertTrue(uqs.hasParameter(book));
+ assertTrue(uqs.hasParameter(price));
+ assertFalse(uqs.hasParameter(notExistPar));
+ assertEquals(bookName, uqs.getValue(book));
+ assertEquals(bookPrice, uqs.getValue(price));
+ assertNull(uqs.getValue(notExistPar));
+ uqs.clear();
+ assertFalse(uqs.hasParameter(book));
+ assertFalse(uqs.hasParameter(price));
+
+ uqs.parseEntry(book, bookName);
+ assertTrue(uqs.hasParameter(book));
+ assertEquals(bookName, uqs.getValue(book));
+ uqs.parseEntry(price, bookPrice);
+ assertTrue(uqs.hasParameter(price));
+ assertEquals(bookPrice, uqs.getValue(price));
+ assertFalse(uqs.hasParameter(notExistPar));
+ assertNull(uqs.getValue(notExistPar));
+
+ uqs = new MockUrlQuerySanitizer(TEST_URL);
+ assertTrue(uqs.getAllowUnregisteredParamaters());
+
+ assertTrue(uqs.hasParameter(NAME));
+ assertTrue(uqs.hasParameter(AGE));
+ assertTrue(uqs.hasParameter(HEIGHT));
+ assertFalse(uqs.hasParameter(notExistPar));
+
+ assertEquals(EXPECTED_UNDERLINE_NAME, uqs.getValue(NAME));
+ assertEquals(EXPECTED_AGE, uqs.getValue(AGE));
+ assertEquals(EXPECTED_HEIGHT, uqs.getValue(HEIGHT));
+ assertNull(uqs.getValue(notExistPar));
+
+ final int ContainerLen = 3;
+ Set<String> urlSet = uqs.getParameterSet();
+ assertEquals(ContainerLen, urlSet.size());
+ assertTrue(urlSet.contains(NAME));
+ assertTrue(urlSet.contains(AGE));
+ assertTrue(urlSet.contains(HEIGHT));
+ assertFalse(urlSet.contains(notExistPar));
+
+ List<ParameterValuePair> urlList = uqs.getParameterList();
+ assertEquals(ContainerLen, urlList.size());
+ ParameterValuePair pvp = urlList.get(0);
+ assertEquals(NAME, pvp.mParameter);
+ assertEquals(EXPECTED_UNDERLINE_NAME, pvp.mValue);
+ pvp = urlList.get(1);
+ assertEquals(AGE, pvp.mParameter);
+ assertEquals(EXPECTED_AGE, pvp.mValue);
+ pvp = urlList.get(2);
+ assertEquals(HEIGHT, pvp.mParameter);
+ assertEquals(EXPECTED_HEIGHT, pvp.mValue);
+
+ assertFalse(uqs.getPreferFirstRepeatedParameter());
+ uqs.addSanitizedEntry(HEIGHT, EXPECTED_HEIGHT + 1);
+ assertEquals(ContainerLen, urlSet.size());
+ assertEquals(ContainerLen + 1, urlList.size());
+ assertEquals(EXPECTED_HEIGHT + 1, uqs.getValue(HEIGHT));
+
+ uqs.setPreferFirstRepeatedParameter(true);
+ assertTrue(uqs.getPreferFirstRepeatedParameter());
+ uqs.addSanitizedEntry(HEIGHT, EXPECTED_HEIGHT);
+ assertEquals(ContainerLen, urlSet.size());
+ assertEquals(ContainerLen + 2, urlList.size());
+ assertEquals(EXPECTED_HEIGHT + 1, uqs.getValue(HEIGHT));
+
+ uqs.registerParameter(NAME, null);
+ assertNull(uqs.getValueSanitizer(NAME));
+ assertNotNull(uqs.getEffectiveValueSanitizer(NAME));
+
+ uqs.setAllowUnregisteredParamaters(false);
+ assertFalse(uqs.getAllowUnregisteredParamaters());
+ uqs.registerParameter(NAME, null);
+ assertNull(uqs.getEffectiveValueSanitizer(NAME));
+
+ ValueSanitizer vs = new IllegalCharacterValueSanitizer(ALL_OK);
+ uqs.registerParameter(NAME, vs);
+ uqs.parseUrl(TEST_URL);
+ assertEquals(EXPECTED_SPACE_NAME, uqs.getValue(NAME));
+ assertNotSame(EXPECTED_AGE, uqs.getValue(AGE));
+
+ String[] register = {NAME, AGE};
+ uqs.registerParameters(register, vs);
+ uqs.parseUrl(TEST_URL);
+ assertEquals(EXPECTED_SPACE_NAME, uqs.getValue(NAME));
+ assertEquals(EXPECTED_AGE, uqs.getValue(AGE));
+ assertNotSame(EXPECTED_HEIGHT, uqs.getValue(HEIGHT));
+
+ uqs.setUnregisteredParameterValueSanitizer(vs);
+ assertEquals(vs, uqs.getUnregisteredParameterValueSanitizer());
+
+ vs = UrlQuerySanitizer.getAllIllegal();
+ assertEquals("Joe_User", vs.sanitize("Joe<User"));
+ vs = UrlQuerySanitizer.getAllButNulAndAngleBracketsLegal();
+ assertEquals("Joe User", vs.sanitize("Joe<>\0User"));
+ vs = UrlQuerySanitizer.getAllButNulLegal();
+ assertEquals("Joe User", vs.sanitize("Joe\0User"));
+ vs = UrlQuerySanitizer.getAllButWhitespaceLegal();
+ assertEquals("Joe_User", vs.sanitize("Joe User"));
+ vs = UrlQuerySanitizer.getAmpAndSpaceLegal();
+ assertEquals("Joe User&", vs.sanitize("Joe User&"));
+ vs = UrlQuerySanitizer.getAmpLegal();
+ assertEquals("Joe_User&", vs.sanitize("Joe User&"));
+ vs = UrlQuerySanitizer.getSpaceLegal();
+ assertEquals("Joe User ", vs.sanitize("Joe User&"));
+ vs = UrlQuerySanitizer.getUrlAndSpaceLegal();
+ assertEquals("Joe User&Smith%B5'\'", vs.sanitize("Joe User&Smith%B5'\'"));
+ vs = UrlQuerySanitizer.getUrlLegal();
+ assertEquals("Joe_User&Smith%B5'\'", vs.sanitize("Joe User&Smith%B5'\'"));
+
+ String escape = "Joe";
+ assertEquals(escape, uqs.unescape(escape));
+ String expectedPlus = "Joe User";
+ String expectedPercentSignHex = "title=" + Character.toString((char)181);
+ String initialPlus = "Joe+User";
+ String initialPercentSign = "title=%B5";
+ assertEquals(expectedPlus, uqs.unescape(initialPlus));
+ assertEquals(expectedPercentSignHex, uqs.unescape(initialPercentSign));
+ String expectedPlusThenPercentSign = "Joe Random, User";
+ String plusThenPercentSign = "Joe+Random%2C%20User";
+ assertEquals(expectedPlusThenPercentSign, uqs.unescape(plusThenPercentSign));
+ String expectedPercentSignThenPlus = "Joe, Random User";
+ String percentSignThenPlus = "Joe%2C+Random+User";
+ assertEquals(expectedPercentSignThenPlus, uqs.unescape(percentSignThenPlus));
+
+ assertTrue(uqs.decodeHexDigit('0') >= 0);
+ assertTrue(uqs.decodeHexDigit('b') >= 0);
+ assertTrue(uqs.decodeHexDigit('F') >= 0);
+ assertTrue(uqs.decodeHexDigit('$') < 0);
+
+ assertTrue(uqs.isHexDigit('0'));
+ assertTrue(uqs.isHexDigit('b'));
+ assertTrue(uqs.isHexDigit('F'));
+ assertFalse(uqs.isHexDigit('$'));
+
+ uqs.clear();
+ assertEquals(0, urlSet.size());
+ assertEquals(0, urlList.size());
+
+ uqs.setPreferFirstRepeatedParameter(true);
+ assertTrue(uqs.getPreferFirstRepeatedParameter());
+ uqs.setPreferFirstRepeatedParameter(false);
+ assertFalse(uqs.getPreferFirstRepeatedParameter());
+
+ UrlQuerySanitizer uq = new UrlQuerySanitizer();
+ uq.setPreferFirstRepeatedParameter(true);
+ final String PARA_ANSWER = "answer";
+ uq.registerParameter(PARA_ANSWER, new MockValueSanitizer());
+ uq.parseUrl("http://www.google.com/question?answer=13&answer=42");
+ assertEquals("13", uq.getValue(PARA_ANSWER));
+
+ uq.setPreferFirstRepeatedParameter(false);
+ uq.parseQuery("http://www.google.com/question?answer=13&answer=42");
+ assertEquals("42", uq.getValue(PARA_ANSWER));
+
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // Only fixed in R
+ public void testScriptUrlOk_73822755() {
+ ValueSanitizer sanitizer = new UrlQuerySanitizer.IllegalCharacterValueSanitizer(
+ UrlQuerySanitizer.IllegalCharacterValueSanitizer.SCRIPT_URL_OK);
+ assertEquals("javascript:alert()", sanitizer.sanitize("javascript:alert()"));
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // Only fixed in R
+ public void testScriptUrlBlocked_73822755() {
+ ValueSanitizer sanitizer = UrlQuerySanitizer.getUrlAndSpaceLegal();
+ assertEquals("", sanitizer.sanitize("javascript:alert()"));
+ }
+
+ private static class MockValueSanitizer implements ValueSanitizer{
+
+ public String sanitize(String value) {
+ return value;
+ }
+ }
+
+ class MockUrlQuerySanitizer extends UrlQuerySanitizer {
+ public MockUrlQuerySanitizer() {
+ super();
+ }
+
+ public MockUrlQuerySanitizer(String url) {
+ super(url);
+ }
+
+ @Override
+ protected void addSanitizedEntry(String parameter, String value) {
+ super.addSanitizedEntry(parameter, value);
+ }
+
+ @Override
+ protected void clear() {
+ super.clear();
+ }
+
+ @Override
+ protected int decodeHexDigit(char c) {
+ return super.decodeHexDigit(c);
+ }
+
+ @Override
+ protected boolean isHexDigit(char c) {
+ return super.isHexDigit(c);
+ }
+
+ @Override
+ protected void parseEntry(String parameter, String value) {
+ super.parseEntry(parameter, value);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java
new file mode 100644
index 0000000..f86af31
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.UrlQuerySanitizer;
+import android.net.UrlQuerySanitizer.IllegalCharacterValueSanitizer;
+import android.test.AndroidTestCase;
+
+public class UrlQuerySanitizer_IllegalCharacterValueSanitizerTest extends AndroidTestCase {
+ static final int SPACE_OK = IllegalCharacterValueSanitizer.SPACE_OK;
+ public void testSanitize() {
+ IllegalCharacterValueSanitizer sanitizer = new IllegalCharacterValueSanitizer(SPACE_OK);
+ assertEquals("Joe User", sanitizer.sanitize("Joe<User"));
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_ParameterValuePairTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_ParameterValuePairTest.java
new file mode 100644
index 0000000..077cdaf
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_ParameterValuePairTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.net.UrlQuerySanitizer;
+import android.net.UrlQuerySanitizer.ParameterValuePair;
+import android.test.AndroidTestCase;
+
+public class UrlQuerySanitizer_ParameterValuePairTest extends AndroidTestCase {
+ public void testConstructor() {
+ final String parameter = "name";
+ final String vaule = "Joe_user";
+
+ UrlQuerySanitizer uqs = new UrlQuerySanitizer();
+ ParameterValuePair parameterValuePair = uqs.new ParameterValuePair(parameter, vaule);
+ assertEquals(parameter, parameterValuePair.mParameter);
+ assertEquals(vaule, parameterValuePair.mValue);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
new file mode 100644
index 0000000..5c7b5ca
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts;
+
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+import java.net.DatagramSocket;
+import java.net.Socket;
+
+/**
+ * VpnService API is built with security in mind. However, its security also
+ * blocks us from writing tests for positive cases. For now we only test for
+ * negative cases, and we will try to cover the rest in the future.
+ */
+public class VpnServiceTest extends AndroidTestCase {
+
+ private static final String TAG = VpnServiceTest.class.getSimpleName();
+
+ private VpnService mVpnService = new VpnService();
+
+ @AppModeFull(reason = "PackageManager#queryIntentActivities cannot access in instant app mode")
+ public void testPrepare() throws Exception {
+ // Should never return null since we are not prepared.
+ Intent intent = VpnService.prepare(mContext);
+ assertNotNull(intent);
+
+ // Should be always resolved by only one activity.
+ int count = mContext.getPackageManager().queryIntentActivities(intent, 0).size();
+ assertEquals(1, count);
+ }
+
+ @AppModeFull(reason = "establish() requires prepare(), which requires PackageManager access")
+ public void testEstablish() throws Exception {
+ ParcelFileDescriptor descriptor = null;
+ try {
+ // Should always return null since we are not prepared.
+ descriptor = mVpnService.new Builder().addAddress("8.8.8.8", 30).establish();
+ assertNull(descriptor);
+ } finally {
+ try {
+ descriptor.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
+ public void testProtect_DatagramSocket() throws Exception {
+ DatagramSocket socket = new DatagramSocket();
+ try {
+ // Should always return false since we are not prepared.
+ assertFalse(mVpnService.protect(socket));
+ } finally {
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
+ public void testProtect_Socket() throws Exception {
+ Socket socket = new Socket();
+ try {
+ // Should always return false since we are not prepared.
+ assertFalse(mVpnService.protect(socket));
+ } finally {
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
+ public void testProtect_int() throws Exception {
+ DatagramSocket socket = new DatagramSocket();
+ ParcelFileDescriptor descriptor = ParcelFileDescriptor.fromDatagramSocket(socket);
+ try {
+ // Should always return false since we are not prepared.
+ assertFalse(mVpnService.protect(descriptor.getFd()));
+ } finally {
+ try {
+ descriptor.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ public void testTunDevice() throws Exception {
+ File file = new File("/dev/tun");
+ assertTrue(file.exists());
+ assertFalse(file.isFile());
+ assertFalse(file.isDirectory());
+ assertFalse(file.canExecute());
+ assertFalse(file.canRead());
+ assertFalse(file.canWrite());
+ }
+}
diff --git a/tests/cts/net/src/android/net/ipv6/cts/PingTest.java b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
new file mode 100644
index 0000000..146fd83
--- /dev/null
+++ b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ipv6.cts;
+
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import static android.system.OsConstants.*;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Checks that the device has kernel support for the IPv6 ping socket. This allows ping6 to work
+ * without root privileges. The necessary kernel code is in Linux 3.11 or above, or the
+ * <code>common/android-3.x</code> kernel trees. If you are not running one of these kernels, the
+ * functionality can be obtained by cherry-picking the following patches from David Miller's
+ * <code>net-next</code> tree:
+ * <ul>
+ * <li>6d0bfe2 net: ipv6: Add IPv6 support to the ping socket.
+ * <li>c26d6b4 ping: always initialize ->sin6_scope_id and ->sin6_flowinfo
+ * <li>fbfe80c net: ipv6: fix wrong ping_v6_sendmsg return value
+ * <li>a1bdc45 net: ipv6: add missing lock in ping_v6_sendmsg
+ * <li>cf970c0 ping: prevent NULL pointer dereference on write to msg_name
+ * </ul>
+ * or the equivalent backports to the <code>common/android-3.x</code> trees.
+ */
+public class PingTest extends AndroidTestCase {
+ /** Maximum size of the packets we're using to test. */
+ private static final int MAX_SIZE = 4096;
+
+ /** Size of the ICMPv6 header. */
+ private static final int ICMP_HEADER_SIZE = 8;
+
+ /** Number of packets to test. */
+ private static final int NUM_PACKETS = 10;
+
+ /** The beginning of an ICMPv6 echo request: type, code, and uninitialized checksum. */
+ private static final byte[] PING_HEADER = new byte[] {
+ (byte) ICMP6_ECHO_REQUEST, (byte) 0x00, (byte) 0x00, (byte) 0x00
+ };
+
+ /**
+ * Returns a byte array containing an ICMPv6 echo request with the specified payload length.
+ */
+ private byte[] pingPacket(int payloadLength) {
+ byte[] packet = new byte[payloadLength + ICMP_HEADER_SIZE];
+ new Random().nextBytes(packet);
+ System.arraycopy(PING_HEADER, 0, packet, 0, PING_HEADER.length);
+ return packet;
+ }
+
+ /**
+ * Checks that the first length bytes of two byte arrays are equal.
+ */
+ private void assertArrayBytesEqual(byte[] expected, byte[] actual, int length) {
+ for (int i = 0; i < length; i++) {
+ assertEquals("Arrays differ at index " + i + ":", expected[i], actual[i]);
+ }
+ }
+
+ /**
+ * Creates an IPv6 ping socket and sets a receive timeout of 100ms.
+ */
+ private FileDescriptor createPingSocket() throws ErrnoException {
+ FileDescriptor s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+ Os.setsockoptTimeval(s, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(100));
+ return s;
+ }
+
+ /**
+ * Sends a ping packet to a random port on the specified address on the specified socket.
+ */
+ private void sendPing(FileDescriptor s,
+ InetAddress address, byte[] packet) throws ErrnoException, IOException {
+ // Pick a random port. Choose a range that gives a reasonable chance of picking a low port.
+ int port = (int) (Math.random() * 2048);
+
+ // Send the packet.
+ int ret = Os.sendto(s, ByteBuffer.wrap(packet), 0, address, port);
+ assertEquals(packet.length, ret);
+ }
+
+ /**
+ * Checks that a socket has received a response appropriate to the specified packet.
+ */
+ private void checkResponse(FileDescriptor s, InetAddress dest,
+ byte[] sent, boolean useRecvfrom) throws ErrnoException, IOException {
+ ByteBuffer responseBuffer = ByteBuffer.allocate(MAX_SIZE);
+ int bytesRead;
+
+ // Receive the response.
+ if (useRecvfrom) {
+ InetSocketAddress from = new InetSocketAddress();
+ bytesRead = Os.recvfrom(s, responseBuffer, 0, from);
+
+ // Check the source address and scope ID.
+ assertTrue(from.getAddress() instanceof Inet6Address);
+ Inet6Address fromAddress = (Inet6Address) from.getAddress();
+ assertEquals(0, fromAddress.getScopeId());
+ assertNull(fromAddress.getScopedInterface());
+ assertEquals(dest.getHostAddress(), fromAddress.getHostAddress());
+ } else {
+ bytesRead = Os.read(s, responseBuffer);
+ }
+
+ // Check the packet length.
+ assertEquals(sent.length, bytesRead);
+
+ // Check the response is an echo reply.
+ byte[] response = new byte[bytesRead];
+ responseBuffer.flip();
+ responseBuffer.get(response, 0, bytesRead);
+ assertEquals((byte) ICMP6_ECHO_REPLY, response[0]);
+
+ // Find out what ICMP ID was used in the packet that was sent.
+ int id = ((InetSocketAddress) Os.getsockname(s)).getPort();
+ sent[4] = (byte) (id / 256);
+ sent[5] = (byte) (id % 256);
+
+ // Ensure the response is the same as the packet, except for the type (which is 0x81)
+ // and the ID and checksum, which are set by the kernel.
+ response[0] = (byte) 0x80; // Type.
+ response[2] = response[3] = (byte) 0x00; // Checksum.
+ assertArrayBytesEqual(response, sent, bytesRead);
+ }
+
+ /**
+ * Sends NUM_PACKETS random ping packets to ::1 and checks the replies.
+ */
+ public void testLoopbackPing() throws ErrnoException, IOException {
+ // Generate a random ping packet and send it to localhost.
+ InetAddress ipv6Loopback = InetAddress.getByName(null);
+ assertEquals("::1", ipv6Loopback.getHostAddress());
+
+ for (int i = 0; i < NUM_PACKETS; i++) {
+ byte[] packet = pingPacket((int) (Math.random() * (MAX_SIZE - ICMP_HEADER_SIZE)));
+ FileDescriptor s = createPingSocket();
+ // Use both recvfrom and read().
+ sendPing(s, ipv6Loopback, packet);
+ checkResponse(s, ipv6Loopback, packet, true);
+ sendPing(s, ipv6Loopback, packet);
+ checkResponse(s, ipv6Loopback, packet, false);
+ // Check closing the socket doesn't raise an exception.
+ Os.close(s);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java
new file mode 100644
index 0000000..412498c
--- /dev/null
+++ b/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.rtp.cts;
+
+import android.net.rtp.AudioCodec;
+import android.test.AndroidTestCase;
+
+public class AudioCodecTest extends AndroidTestCase {
+
+ private void assertEquals(AudioCodec codec, int type, String rtpmap, String fmtp) {
+ if (type >= 0) {
+ assertEquals(codec.type, type);
+ } else {
+ assertTrue(codec.type >= 96 && codec.type <= 127);
+ }
+ assertEquals(codec.rtpmap.compareToIgnoreCase(rtpmap), 0);
+ assertEquals(codec.fmtp, fmtp);
+ }
+
+ public void testConstants() throws Exception {
+ assertEquals(AudioCodec.PCMU, 0, "PCMU/8000", null);
+ assertEquals(AudioCodec.PCMA, 8, "PCMA/8000", null);
+ assertEquals(AudioCodec.GSM, 3, "GSM/8000", null);
+ assertEquals(AudioCodec.GSM_EFR, -1, "GSM-EFR/8000", null);
+ assertEquals(AudioCodec.AMR, -1, "AMR/8000", null);
+
+ assertFalse(AudioCodec.AMR.type == AudioCodec.GSM_EFR.type);
+ }
+
+ public void testGetCodec() throws Exception {
+ // Bad types.
+ assertNull(AudioCodec.getCodec(128, "PCMU/8000", null));
+ assertNull(AudioCodec.getCodec(-1, "PCMU/8000", null));
+ assertNull(AudioCodec.getCodec(96, null, null));
+
+ // Fixed types.
+ assertEquals(AudioCodec.getCodec(0, null, null), 0, "PCMU/8000", null);
+ assertEquals(AudioCodec.getCodec(8, null, null), 8, "PCMA/8000", null);
+ assertEquals(AudioCodec.getCodec(3, null, null), 3, "GSM/8000", null);
+
+ // Dynamic types.
+ assertEquals(AudioCodec.getCodec(96, "pcmu/8000", null), 96, "PCMU/8000", null);
+ assertEquals(AudioCodec.getCodec(97, "pcma/8000", null), 97, "PCMA/8000", null);
+ assertEquals(AudioCodec.getCodec(98, "gsm/8000", null), 98, "GSM/8000", null);
+ assertEquals(AudioCodec.getCodec(99, "gsm-efr/8000", null), 99, "GSM-EFR/8000", null);
+ assertEquals(AudioCodec.getCodec(100, "amr/8000", null), 100, "AMR/8000", null);
+ }
+
+ public void testGetCodecs() throws Exception {
+ AudioCodec[] codecs = AudioCodec.getCodecs();
+ assertTrue(codecs.length >= 5);
+
+ // The types of the codecs should be different.
+ boolean[] types = new boolean[128];
+ for (AudioCodec codec : codecs) {
+ assertFalse(types[codec.type]);
+ types[codec.type] = true;
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java
new file mode 100644
index 0000000..fc78e96
--- /dev/null
+++ b/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.rtp.cts;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.net.rtp.RtpStream;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+
+import androidx.core.os.BuildCompat;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+
+@AppModeFull(reason = "RtpStream cannot create in instant app mode")
+public class AudioGroupTest extends AndroidTestCase {
+
+ private static final String TAG = AudioGroupTest.class.getSimpleName();
+
+ private AudioManager mAudioManager;
+
+ private AudioStream mStreamA;
+ private DatagramSocket mSocketA;
+ private AudioStream mStreamB;
+ private DatagramSocket mSocketB;
+ private AudioGroup mGroup;
+
+ @Override
+ public void setUp() throws Exception {
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+
+ InetAddress local = InetAddress.getByName("::1");
+
+ mStreamA = new AudioStream(local);
+ mStreamA.setMode(RtpStream.MODE_NORMAL);
+ mStreamA.setCodec(AudioCodec.PCMU);
+ mSocketA = new DatagramSocket();
+ mSocketA.connect(mStreamA.getLocalAddress(), mStreamA.getLocalPort());
+ mStreamA.associate(mSocketA.getLocalAddress(), mSocketA.getLocalPort());
+
+ mStreamB = new AudioStream(local);
+ mStreamB.setMode(RtpStream.MODE_NORMAL);
+ mStreamB.setCodec(AudioCodec.PCMU);
+ mSocketB = new DatagramSocket();
+ mSocketB.connect(mStreamB.getLocalAddress(), mStreamB.getLocalPort());
+ mStreamB.associate(mSocketB.getLocalAddress(), mSocketB.getLocalPort());
+
+ // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
+ mGroup = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR()
+ ? new AudioGroup(mContext)
+ : new AudioGroup(); // Constructor with context argument was introduced in R
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mGroup.clear();
+ mStreamA.release();
+ mSocketA.close();
+ mStreamB.release();
+ mSocketB.close();
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ }
+
+ private void assertPacket(DatagramSocket socket, int length) throws Exception {
+ DatagramPacket packet = new DatagramPacket(new byte[length + 1], length + 1);
+ socket.setSoTimeout(3000);
+ socket.receive(packet);
+ assertEquals(packet.getLength(), length);
+ }
+
+ private void drain(DatagramSocket socket) throws Exception {
+ DatagramPacket packet = new DatagramPacket(new byte[1], 1);
+ socket.setSoTimeout(1);
+ try {
+ // Drain the socket by retrieving all the packets queued on it.
+ // A SocketTimeoutException will be thrown when it becomes empty.
+ while (true) {
+ socket.receive(packet);
+ }
+ } catch (Exception e) {
+ // ignore.
+ }
+ }
+
+ public void testTraffic() throws Exception {
+ mStreamA.join(mGroup);
+ assertPacket(mSocketA, 12 + 160);
+
+ mStreamB.join(mGroup);
+ assertPacket(mSocketB, 12 + 160);
+
+ mStreamA.join(null);
+ drain(mSocketA);
+
+ drain(mSocketB);
+ assertPacket(mSocketB, 12 + 160);
+
+ mStreamA.join(mGroup);
+ assertPacket(mSocketA, 12 + 160);
+ }
+
+ public void testSetMode() throws Exception {
+ mGroup.setMode(AudioGroup.MODE_NORMAL);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_NORMAL);
+
+ mGroup.setMode(AudioGroup.MODE_MUTED);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_MUTED);
+
+ mStreamA.join(mGroup);
+ mStreamB.join(mGroup);
+
+ mGroup.setMode(AudioGroup.MODE_NORMAL);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_NORMAL);
+
+ mGroup.setMode(AudioGroup.MODE_MUTED);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_MUTED);
+ }
+
+ public void testAdd() throws Exception {
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 1);
+
+ mStreamB.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 2);
+
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 2);
+ }
+
+ public void testRemove() throws Exception {
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 1);
+
+ mStreamA.join(null);
+ assertEquals(mGroup.getStreams().length, 0);
+
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 1);
+ }
+
+ public void testClear() throws Exception {
+ mStreamA.join(mGroup);
+ mStreamB.join(mGroup);
+ mGroup.clear();
+
+ assertEquals(mGroup.getStreams().length, 0);
+ assertFalse(mStreamA.isBusy());
+ assertFalse(mStreamB.isBusy());
+ }
+
+ public void testDoubleClear() throws Exception {
+ mStreamA.join(mGroup);
+ mStreamB.join(mGroup);
+ mGroup.clear();
+ mGroup.clear();
+ }
+}
diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java
new file mode 100644
index 0000000..f2db6ee
--- /dev/null
+++ b/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.rtp.cts;
+
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioStream;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+
+import java.net.InetAddress;
+
+@AppModeFull(reason = "RtpStream cannot create in instant app mode")
+public class AudioStreamTest extends AndroidTestCase {
+
+ private void testRtpStream(InetAddress address) throws Exception {
+ AudioStream stream = new AudioStream(address);
+ assertEquals(stream.getLocalAddress(), address);
+ assertEquals(stream.getLocalPort() % 2, 0);
+
+ assertNull(stream.getRemoteAddress());
+ assertEquals(stream.getRemotePort(), -1);
+ stream.associate(address, 1000);
+ assertEquals(stream.getRemoteAddress(), address);
+ assertEquals(stream.getRemotePort(), 1000);
+
+ assertFalse(stream.isBusy());
+ stream.release();
+ }
+
+ public void testV4Stream() throws Exception {
+ testRtpStream(InetAddress.getByName("127.0.0.1"));
+ }
+
+ public void testV6Stream() throws Exception {
+ testRtpStream(InetAddress.getByName("::1"));
+ }
+
+ public void testSetDtmfType() throws Exception {
+ AudioStream stream = new AudioStream(InetAddress.getByName("::1"));
+
+ assertEquals(stream.getDtmfType(), -1);
+ try {
+ stream.setDtmfType(0);
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ stream.setDtmfType(96);
+ assertEquals(stream.getDtmfType(), 96);
+
+ stream.setCodec(AudioCodec.getCodec(97, "PCMU/8000", null));
+ try {
+ stream.setDtmfType(97);
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ stream.release();
+ }
+
+ public void testSetCodec() throws Exception {
+ AudioStream stream = new AudioStream(InetAddress.getByName("::1"));
+
+ assertNull(stream.getCodec());
+ stream.setCodec(AudioCodec.getCodec(97, "PCMU/8000", null));
+ assertNotNull(stream.getCodec());
+
+ stream.setDtmfType(96);
+ try {
+ stream.setCodec(AudioCodec.getCodec(96, "PCMU/8000", null));
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ stream.release();
+ }
+
+ public void testDoubleRelease() throws Exception {
+ AudioStream stream = new AudioStream(InetAddress.getByName("::1"));
+ stream.release();
+ stream.release();
+ }
+}
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
new file mode 100644
index 0000000..88a2068
--- /dev/null
+++ b/tests/cts/net/util/Android.bp
@@ -0,0 +1,30 @@
+//
+// 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.
+//
+
+// Common utilities for cts net tests.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "cts-net-utils",
+ srcs: ["java/**/*.java", "java/**/*.kt"],
+ static_libs: [
+ "compatibility-device-util-axt",
+ "junit",
+ "net-tests-utils",
+ ],
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
new file mode 100644
index 0000000..d5a26c4
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -0,0 +1,694 @@
+/*
+ * 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 android.net.cts.util;
+
+import static android.Manifest.permission.ACCESS_WIFI_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.net.NetworkRequest;
+import android.net.TestNetworkManager;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import junit.framework.AssertionFailedError;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public final class CtsNetUtils {
+ private static final String TAG = CtsNetUtils.class.getSimpleName();
+ private static final int DURATION = 10000;
+ private static final int SOCKET_TIMEOUT_MS = 2000;
+ private static final int PRIVATE_DNS_PROBE_MS = 1_000;
+
+ private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
+ private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
+ private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+ public static final int HTTP_PORT = 80;
+ public static final String TEST_HOST = "connectivitycheck.gstatic.com";
+ public static final String HTTP_REQUEST =
+ "GET /generate_204 HTTP/1.0\r\n" +
+ "Host: " + TEST_HOST + "\r\n" +
+ "Connection: keep-alive\r\n\r\n";
+ // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent.
+ public static final String NETWORK_CALLBACK_ACTION =
+ "ConnectivityManagerTest.NetworkCallbackAction";
+
+ private final IBinder mBinder = new Binder();
+ private final Context mContext;
+ private final ConnectivityManager mCm;
+ private final ContentResolver mCR;
+ private final WifiManager mWifiManager;
+ private TestNetworkCallback mCellNetworkCallback;
+ private String mOldPrivateDnsMode;
+ private String mOldPrivateDnsSpecifier;
+
+ public CtsNetUtils(Context context) {
+ mContext = context;
+ mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ mCR = context.getContentResolver();
+ }
+
+ /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */
+ public boolean hasIpsecTunnelsFeature() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+ || SystemProperties.getInt("ro.product.first_api_level", 0)
+ >= Build.VERSION_CODES.Q;
+ }
+
+ /**
+ * Sets the given appop using shell commands
+ *
+ * <p>Expects caller to hold the shell permission identity.
+ */
+ public void setAppopPrivileged(int appop, boolean allow) {
+ final String opName = AppOpsManager.opToName(appop);
+ for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) {
+ final String cmd =
+ String.format(
+ "appops set %s %s %s",
+ pkg, // Package name
+ opName, // Appop
+ (allow ? "allow" : "deny")); // Action
+ SystemUtil.runShellCommand(cmd);
+ }
+ }
+
+ /** Sets up a test network using the provided interface name */
+ public TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception {
+ // Build a network request
+ final NetworkRequest nr =
+ new NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(ifname)
+ .build();
+
+ final TestNetworkCallback cb = new TestNetworkCallback();
+ mCm.requestNetwork(nr, cb);
+
+ // Setup the test network after network request is filed to prevent Network from being
+ // reaped due to no requests matching it.
+ mContext.getSystemService(TestNetworkManager.class).setupTestNetwork(ifname, mBinder);
+
+ return cb;
+ }
+
+ // Toggle WiFi twice, leaving it in the state it started in
+ public void toggleWifi() {
+ if (mWifiManager.isWifiEnabled()) {
+ Network wifiNetwork = getWifiNetwork();
+ disconnectFromWifi(wifiNetwork);
+ connectToWifi();
+ } else {
+ connectToWifi();
+ Network wifiNetwork = getWifiNetwork();
+ disconnectFromWifi(wifiNetwork);
+ }
+ }
+
+ /**
+ * Enable WiFi and wait for it to become connected to a network.
+ *
+ * This method expects to receive a legacy broadcast on connect, which may not be sent if the
+ * network does not become default or if it is not the first network.
+ */
+ public Network connectToWifi() {
+ return connectToWifi(true /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Enable WiFi and wait for it to become connected to a network.
+ *
+ * A network is considered connected when a {@link NetworkRequest} with TRANSPORT_WIFI
+ * receives a {@link NetworkCallback#onAvailable(Network)} callback.
+ */
+ public Network ensureWifiConnected() {
+ return connectToWifi(false /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Enable WiFi and wait for it to become connected to a network.
+ *
+ * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION connected
+ * broadcast. The broadcast is typically not sent if the network
+ * does not become the default network, and is not the first
+ * network to appear.
+ * @return The network that was newly connected.
+ */
+ private Network connectToWifi(boolean expectLegacyBroadcast) {
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+ Network wifiNetwork = null;
+
+ ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(receiver, filter);
+
+ boolean connected = false;
+ final String err = "Wifi must be configured to connect to an access point for this test";
+ try {
+ clearWifiBlacklist();
+ SystemUtil.runShellCommand("svc wifi enable");
+ final WifiConfiguration config = maybeAddVirtualWifiConfiguration();
+ if (config == null) {
+ // TODO: this may not clear the BSSID blacklist, as opposed to
+ // mWifiManager.connect(config)
+ assertTrue("Error reconnecting wifi", runAsShell(NETWORK_SETTINGS,
+ mWifiManager::reconnect));
+ } else {
+ // When running CTS, devices are expected to have wifi networks pre-configured.
+ // This condition is only hit on virtual devices.
+ final Integer error = runAsShell(NETWORK_SETTINGS, () -> {
+ final ConnectWifiListener listener = new ConnectWifiListener();
+ mWifiManager.connect(config, listener);
+ return listener.connectFuture.get(
+ CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ });
+ assertNull("Error connecting to wifi: " + error, error);
+ }
+ // Ensure we get an onAvailable callback and possibly a CONNECTIVITY_ACTION.
+ wifiNetwork = callback.waitForAvailable();
+ assertNotNull(err + ": onAvailable callback not received", wifiNetwork);
+ connected = !expectLegacyBroadcast || receiver.waitForState();
+ } catch (InterruptedException ex) {
+ fail("connectToWifi was interrupted");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ mContext.unregisterReceiver(receiver);
+ }
+
+ assertTrue(err + ": CONNECTIVITY_ACTION not received", connected);
+ return wifiNetwork;
+ }
+
+ private static class ConnectWifiListener implements WifiManager.ActionListener {
+ /**
+ * Future completed when the connect process ends. Provides the error code or null if none.
+ */
+ final CompletableFuture<Integer> connectFuture = new CompletableFuture<>();
+ @Override
+ public void onSuccess() {
+ connectFuture.complete(null);
+ }
+
+ @Override
+ public void onFailure(int reason) {
+ connectFuture.complete(reason);
+ }
+ }
+
+ private WifiConfiguration maybeAddVirtualWifiConfiguration() {
+ final List<WifiConfiguration> configs = runAsShell(NETWORK_SETTINGS,
+ mWifiManager::getConfiguredNetworks);
+ // If no network is configured, add a config for virtual access points if applicable
+ if (configs.size() == 0) {
+ final List<ScanResult> scanResults = getWifiScanResults();
+ final WifiConfiguration virtualConfig = maybeConfigureVirtualNetwork(scanResults);
+ assertNotNull("The device has no configured wifi network", virtualConfig);
+
+ return virtualConfig;
+ }
+ // No need to add a configuration: there is already one
+ return null;
+ }
+
+ private List<ScanResult> getWifiScanResults() {
+ final CompletableFuture<List<ScanResult>> scanResultsFuture = new CompletableFuture<>();
+ runAsShell(NETWORK_SETTINGS, () -> {
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ scanResultsFuture.complete(mWifiManager.getScanResults());
+ }
+ };
+ mContext.registerReceiver(receiver, new IntentFilter(SCAN_RESULTS_AVAILABLE_ACTION));
+ mWifiManager.startScan();
+ });
+
+ try {
+ return scanResultsFuture.get(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ throw new AssertionFailedError("Wifi scan results not received within timeout");
+ }
+ }
+
+ /**
+ * If a virtual wifi network is detected, add a configuration for that network.
+ * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
+ */
+ private WifiConfiguration maybeConfigureVirtualNetwork(List<ScanResult> scanResults) {
+ // Virtual wifi networks used on the emulator and cloud testing infrastructure
+ final List<String> virtualSsids = Arrays.asList("VirtWifi", "AndroidWifi");
+ Log.d(TAG, "Wifi scan results: " + scanResults);
+ final ScanResult virtualScanResult = scanResults.stream().filter(
+ s -> virtualSsids.contains(s.SSID)).findFirst().orElse(null);
+
+ // Only add the virtual configuration if the virtual AP is detected in scans
+ if (virtualScanResult == null) return null;
+
+ final WifiConfiguration virtualConfig = new WifiConfiguration();
+ // ASCII SSIDs need to be surrounded by double quotes
+ virtualConfig.SSID = "\"" + virtualScanResult.SSID + "\"";
+ virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+
+ runAsShell(NETWORK_SETTINGS, () -> {
+ final int networkId = mWifiManager.addNetwork(virtualConfig);
+ assertTrue(networkId >= 0);
+ assertTrue(mWifiManager.enableNetwork(networkId, false /* attemptConnect */));
+ });
+ return virtualConfig;
+ }
+
+ /**
+ * Re-enable wifi networks that were blacklisted, typically because no internet connection was
+ * detected the last time they were connected. This is necessary to make sure wifi can reconnect
+ * to them.
+ */
+ private void clearWifiBlacklist() {
+ runAsShell(NETWORK_SETTINGS, ACCESS_WIFI_STATE, () -> {
+ for (WifiConfiguration cfg : mWifiManager.getConfiguredNetworks()) {
+ assertTrue(mWifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */));
+ }
+ });
+ }
+
+ /**
+ * Disable WiFi and wait for it to become disconnected from the network.
+ *
+ * This method expects to receive a legacy broadcast on disconnect, which may not be sent if the
+ * network was not default, or was not the first network.
+ *
+ * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
+ * is expected to be able to establish a TCP connection to a remote
+ * server before disconnecting, and to have that connection closed in
+ * the process.
+ */
+ public void disconnectFromWifi(Network wifiNetworkToCheck) {
+ disconnectFromWifi(wifiNetworkToCheck, true /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Disable WiFi and wait for it to become disconnected from the network.
+ *
+ * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
+ * is expected to be able to establish a TCP connection to a remote
+ * server before disconnecting, and to have that connection closed in
+ * the process.
+ */
+ public void ensureWifiDisconnected(Network wifiNetworkToCheck) {
+ disconnectFromWifi(wifiNetworkToCheck, false /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Disable WiFi and wait for it to become disconnected from the network.
+ *
+ * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
+ * is expected to be able to establish a TCP connection to a remote
+ * server before disconnecting, and to have that connection closed in
+ * the process.
+ * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION disconnected
+ * broadcast. The broadcast is typically not sent if the network
+ * was not the default network and not the first network to appear.
+ * The check will always be skipped if the device was not connected
+ * to wifi in the first place.
+ */
+ private void disconnectFromWifi(Network wifiNetworkToCheck, boolean expectLegacyBroadcast) {
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+
+ ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.DISCONNECTED);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(receiver, filter);
+
+ final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
+ final boolean wasWifiConnected = wifiInfo != null && wifiInfo.getNetworkId() != -1;
+ // Assert that we can establish a TCP connection on wifi.
+ Socket wifiBoundSocket = null;
+ if (wifiNetworkToCheck != null) {
+ assertTrue("Cannot check network " + wifiNetworkToCheck + ": wifi is not connected",
+ wasWifiConnected);
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(wifiNetworkToCheck);
+ assertNotNull("Network " + wifiNetworkToCheck + " is not connected", nc);
+ try {
+ wifiBoundSocket = getBoundSocket(wifiNetworkToCheck, TEST_HOST, HTTP_PORT);
+ testHttpRequest(wifiBoundSocket);
+ } catch (IOException e) {
+ fail("HTTP request before wifi disconnected failed with: " + e);
+ }
+ }
+
+ try {
+ SystemUtil.runShellCommand("svc wifi disable");
+ if (wasWifiConnected) {
+ // Ensure we get both an onLost callback and a CONNECTIVITY_ACTION.
+ assertNotNull("Did not receive onLost callback after disabling wifi",
+ callback.waitForLost());
+ }
+ if (wasWifiConnected && expectLegacyBroadcast) {
+ assertTrue("Wifi failed to reach DISCONNECTED state.", receiver.waitForState());
+ }
+ } catch (InterruptedException ex) {
+ fail("disconnectFromWifi was interrupted");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ mContext.unregisterReceiver(receiver);
+ }
+
+ // Check that the socket is closed when wifi disconnects.
+ if (wifiBoundSocket != null) {
+ try {
+ testHttpRequest(wifiBoundSocket);
+ fail("HTTP request should not succeed after wifi disconnects");
+ } catch (IOException expected) {
+ assertEquals(Os.strerror(OsConstants.ECONNABORTED), expected.getMessage());
+ }
+ }
+ }
+
+ public Network getWifiNetwork() {
+ TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+ Network network = null;
+ try {
+ network = callback.waitForAvailable();
+ } catch (InterruptedException e) {
+ fail("NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ assertNotNull("Cannot find Network for wifi. Is wifi connected?", network);
+ return network;
+ }
+
+ public Network connectToCell() throws InterruptedException {
+ if (cellConnectAttempted()) {
+ throw new IllegalStateException("Already connected");
+ }
+ NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ mCellNetworkCallback = new TestNetworkCallback();
+ mCm.requestNetwork(cellRequest, mCellNetworkCallback);
+ final Network cellNetwork = mCellNetworkCallback.waitForAvailable();
+ assertNotNull("Cell network not available. " +
+ "Please ensure the device has working mobile data.", cellNetwork);
+ return cellNetwork;
+ }
+
+ public void disconnectFromCell() {
+ if (!cellConnectAttempted()) {
+ throw new IllegalStateException("Cell connection not attempted");
+ }
+ mCm.unregisterNetworkCallback(mCellNetworkCallback);
+ mCellNetworkCallback = null;
+ }
+
+ public boolean cellConnectAttempted() {
+ return mCellNetworkCallback != null;
+ }
+
+ private NetworkRequest makeWifiNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .build();
+ }
+
+ private void testHttpRequest(Socket s) throws IOException {
+ OutputStream out = s.getOutputStream();
+ InputStream in = s.getInputStream();
+
+ final byte[] requestBytes = HTTP_REQUEST.getBytes("UTF-8");
+ byte[] responseBytes = new byte[4096];
+ out.write(requestBytes);
+ in.read(responseBytes);
+ assertTrue(new String(responseBytes, "UTF-8").startsWith("HTTP/1.0 204 No Content\r\n"));
+ }
+
+ private Socket getBoundSocket(Network network, String host, int port) throws IOException {
+ InetSocketAddress addr = new InetSocketAddress(host, port);
+ Socket s = network.getSocketFactory().createSocket();
+ try {
+ s.setSoTimeout(SOCKET_TIMEOUT_MS);
+ s.connect(addr, SOCKET_TIMEOUT_MS);
+ } catch (IOException e) {
+ s.close();
+ throw e;
+ }
+ return s;
+ }
+
+ public void storePrivateDnsSetting() {
+ // Store private DNS setting
+ mOldPrivateDnsMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+ mOldPrivateDnsSpecifier = Settings.Global.getString(mCR,
+ Settings.Global.PRIVATE_DNS_SPECIFIER);
+ // It's possible that there is no private DNS default value in Settings.
+ // Give it a proper default mode which is opportunistic mode.
+ if (mOldPrivateDnsMode == null) {
+ mOldPrivateDnsSpecifier = "";
+ mOldPrivateDnsMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+ Settings.Global.putString(mCR,
+ Settings.Global.PRIVATE_DNS_SPECIFIER, mOldPrivateDnsSpecifier);
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+ }
+ }
+
+ public void restorePrivateDnsSetting() throws InterruptedException {
+ if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) {
+ return;
+ }
+ // restore private DNS setting
+ if ("hostname".equals(mOldPrivateDnsMode)) {
+ setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
+ awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
+ mCm.getActiveNetwork(),
+ mOldPrivateDnsSpecifier, true);
+ } else {
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+ }
+ }
+
+ public void setPrivateDnsStrictMode(String server) {
+ // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
+ // that if the previous private DNS mode was not "hostname", the system only sees one
+ // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server);
+ final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+ // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER.
+ if (!"hostname".equals(mode)) {
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
+ }
+ }
+
+ public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
+ @NonNull String server, boolean requiresValidatedServers) throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+ NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network n, LinkProperties lp) {
+ if (requiresValidatedServers && lp.getValidatedPrivateDnsServers().isEmpty()) {
+ return;
+ }
+ if (network.equals(n) && server.equals(lp.getPrivateDnsServerName())) {
+ latch.countDown();
+ }
+ }
+ };
+ mCm.registerNetworkCallback(request, callback);
+ assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ mCm.unregisterNetworkCallback(callback);
+ // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do
+ // this, then the test could complete before the NetworkMonitor private DNS probe
+ // completes. This would result in tearDown disabling private DNS, and the NetworkMonitor
+ // private DNS probe getting stuck because there are no longer any private DNS servers to
+ // query. This then results in the next test not being able to change the private DNS
+ // setting within the timeout, because the NetworkMonitor thread is blocked in the
+ // private DNS probe. There is no way to know when the probe has completed: because the
+ // network is likely already validated, there is no callback that we can listen to, so
+ // just sleep.
+ if (requiresValidatedServers) {
+ Thread.sleep(PRIVATE_DNS_PROBE_MS);
+ }
+ }
+
+ /**
+ * Receiver that captures the last connectivity change's network type and state. Recognizes
+ * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents.
+ */
+ public static class ConnectivityActionReceiver extends BroadcastReceiver {
+
+ private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
+
+ private final int mNetworkType;
+ private final NetworkInfo.State mNetState;
+ private final ConnectivityManager mCm;
+
+ public ConnectivityActionReceiver(ConnectivityManager cm, int networkType,
+ NetworkInfo.State netState) {
+ this.mCm = cm;
+ mNetworkType = networkType;
+ mNetState = netState;
+ }
+
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ NetworkInfo networkInfo = null;
+
+ // When receiving ConnectivityManager.CONNECTIVITY_ACTION, the NetworkInfo parcelable
+ // is stored in EXTRA_NETWORK_INFO. With a NETWORK_CALLBACK_ACTION, the Network is
+ // sent in EXTRA_NETWORK and we need to ask the ConnectivityManager for the NetworkInfo.
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
+ networkInfo = intent.getExtras()
+ .getParcelable(ConnectivityManager.EXTRA_NETWORK_INFO);
+ assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK_INFO",
+ networkInfo);
+ } else if (NETWORK_CALLBACK_ACTION.equals(action)) {
+ Network network = intent.getExtras()
+ .getParcelable(ConnectivityManager.EXTRA_NETWORK);
+ assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK", network);
+ networkInfo = this.mCm.getNetworkInfo(network);
+ if (networkInfo == null) {
+ // When disconnecting, it seems like we get an intent sent with an invalid
+ // Network; that is, by the time we call ConnectivityManager.getNetworkInfo(),
+ // it is invalid. Ignore these.
+ Log.i(TAG, "ConnectivityActionReceiver NETWORK_CALLBACK_ACTION ignoring "
+ + "invalid network");
+ return;
+ }
+ } else {
+ fail("ConnectivityActionReceiver received unxpected intent action: " + action);
+ }
+
+ assertNotNull("ConnectivityActionReceiver didn't find NetworkInfo", networkInfo);
+ int networkType = networkInfo.getType();
+ State networkState = networkInfo.getState();
+ Log.i(TAG, "Network type: " + networkType + " state: " + networkState);
+ if (networkType == mNetworkType && networkInfo.getState() == mNetState) {
+ mReceiveLatch.countDown();
+ }
+ }
+
+ public boolean waitForState() throws InterruptedException {
+ return mReceiveLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Callback used in testRegisterNetworkCallback that allows caller to block on
+ * {@code onAvailable}.
+ */
+ public static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
+ private final CountDownLatch mAvailableLatch = new CountDownLatch(1);
+ private final CountDownLatch mLostLatch = new CountDownLatch(1);
+ private final CountDownLatch mUnavailableLatch = new CountDownLatch(1);
+
+ public Network currentNetwork;
+ public Network lastLostNetwork;
+
+ public Network waitForAvailable() throws InterruptedException {
+ return mAvailableLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS)
+ ? currentNetwork : null;
+ }
+
+ public Network waitForLost() throws InterruptedException {
+ return mLostLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS)
+ ? lastLostNetwork : null;
+ }
+
+ public boolean waitForUnavailable() throws InterruptedException {
+ return mUnavailableLatch.await(2, TimeUnit.SECONDS);
+ }
+
+
+ @Override
+ public void onAvailable(Network network) {
+ currentNetwork = network;
+ mAvailableLatch.countDown();
+ }
+
+ @Override
+ public void onLost(Network network) {
+ lastLostNetwork = network;
+ if (network.equals(currentNetwork)) {
+ currentNetwork = null;
+ }
+ mLostLatch.countDown();
+ }
+
+ @Override
+ public void onUnavailable() {
+ mUnavailableLatch.countDown();
+ }
+ }
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
new file mode 100644
index 0000000..1bdd533
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -0,0 +1,520 @@
+/*
+ * 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 android.net.cts.util;
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+
+import static org.junit.Assert.assertEquals;
+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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.Network;
+import android.net.TetheredClient;
+import android.net.TetheringInterface;
+import android.net.TetheringManager;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.net.TetheringManager.TetheringInterfaceRegexps;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.wifi.WifiClient;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.SoftApCallback;
+import android.os.ConditionVariable;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.net.module.util.ArrayTrackRecord;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public final class CtsTetheringUtils {
+ private TetheringManager mTm;
+ private WifiManager mWm;
+ private Context mContext;
+
+ private static final int DEFAULT_TIMEOUT_MS = 60_000;
+
+ public CtsTetheringUtils(Context ctx) {
+ mContext = ctx;
+ mTm = mContext.getSystemService(TetheringManager.class);
+ mWm = mContext.getSystemService(WifiManager.class);
+ }
+
+ public static class StartTetheringCallback implements TetheringManager.StartTetheringCallback {
+ private static int TIMEOUT_MS = 30_000;
+ public static class CallbackValue {
+ public final int error;
+
+ private CallbackValue(final int e) {
+ error = e;
+ }
+
+ public static class OnTetheringStarted extends CallbackValue {
+ OnTetheringStarted() { super(TETHER_ERROR_NO_ERROR); }
+ }
+
+ public static class OnTetheringFailed extends CallbackValue {
+ OnTetheringFailed(final int error) { super(error); }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%d)", getClass().getSimpleName(), error);
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+ new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+ @Override
+ public void onTetheringStarted() {
+ mHistory.add(new CallbackValue.OnTetheringStarted());
+ }
+
+ @Override
+ public void onTetheringFailed(final int error) {
+ mHistory.add(new CallbackValue.OnTetheringFailed(error));
+ }
+
+ public void verifyTetheringStarted() {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onTetheringStarted after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Fail start tethering:" + cv,
+ cv instanceof CallbackValue.OnTetheringStarted);
+ }
+
+ public void expectTetheringFailed(final int expected) throws InterruptedException {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+ (cv instanceof CallbackValue.OnTetheringFailed) && (cv.error == expected));
+ }
+ }
+
+ private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) {
+ if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
+
+ for (String regex : ifaceRegexs) {
+ if (iface.matches(regex)) return true;
+ }
+
+ return false;
+ }
+
+ public static boolean isAnyIfaceMatch(final String[] ifaceRegexs, final List<String> ifaces) {
+ if (ifaces == null) return false;
+
+ for (String s : ifaces) {
+ if (isRegexMatch(ifaceRegexs, s)) return true;
+ }
+
+ return false;
+ }
+
+ private static TetheringInterface getFirstMatchingTetheringInterface(final List<String> regexs,
+ final int type, final Set<TetheringInterface> ifaces) {
+ if (ifaces == null || regexs == null) return null;
+
+ final String[] regexArray = regexs.toArray(new String[0]);
+ for (TetheringInterface iface : ifaces) {
+ if (isRegexMatch(regexArray, iface.getInterface()) && type == iface.getType()) {
+ return iface;
+ }
+ }
+
+ return null;
+ }
+
+ // Must poll the callback before looking at the member.
+ public static class TestTetheringEventCallback implements TetheringEventCallback {
+ private static final int TIMEOUT_MS = 30_000;
+
+ public enum CallbackType {
+ ON_SUPPORTED,
+ ON_UPSTREAM,
+ ON_TETHERABLE_REGEX,
+ ON_TETHERABLE_IFACES,
+ ON_TETHERED_IFACES,
+ ON_ERROR,
+ ON_CLIENTS,
+ ON_OFFLOAD_STATUS,
+ };
+
+ public static class CallbackValue {
+ public final CallbackType callbackType;
+ public final Object callbackParam;
+ public final int callbackParam2;
+
+ private CallbackValue(final CallbackType type, final Object param, final int param2) {
+ this.callbackType = type;
+ this.callbackParam = param;
+ this.callbackParam2 = param2;
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue> mHistory =
+ new ArrayTrackRecord<CallbackValue>();
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mCurrent =
+ mHistory.newReadHead();
+
+ private TetheringInterfaceRegexps mTetherableRegex;
+ private List<String> mTetherableIfaces;
+ private List<String> mTetheredIfaces;
+ private String mErrorIface;
+ private int mErrorCode;
+
+ @Override
+ public void onTetheringSupported(boolean supported) {
+ mHistory.add(new CallbackValue(CallbackType.ON_SUPPORTED, null, (supported ? 1 : 0)));
+ }
+
+ @Override
+ public void onUpstreamChanged(Network network) {
+ mHistory.add(new CallbackValue(CallbackType.ON_UPSTREAM, network, 0));
+ }
+
+ @Override
+ public void onTetherableInterfaceRegexpsChanged(TetheringInterfaceRegexps reg) {
+ mTetherableRegex = reg;
+ mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_REGEX, reg, 0));
+ }
+
+ @Override
+ public void onTetherableInterfacesChanged(List<String> interfaces) {
+ mTetherableIfaces = interfaces;
+ }
+ // Call the interface default implementation, which will call
+ // onTetherableInterfacesChanged(List<String>). This ensures that the default implementation
+ // of the new callback method calls the old callback method and avoids the need to convert
+ // Set<TetheringInterface> to List<String> in this code.
+ @Override
+ public void onTetherableInterfacesChanged(Set<TetheringInterface> interfaces) {
+ TetheringEventCallback.super.onTetherableInterfacesChanged(interfaces);
+ assertHasAllTetheringInterfaces(interfaces, mTetherableIfaces);
+ mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0));
+ }
+
+ @Override
+ public void onTetheredInterfacesChanged(List<String> interfaces) {
+ mTetheredIfaces = interfaces;
+ }
+
+ @Override
+ public void onTetheredInterfacesChanged(Set<TetheringInterface> interfaces) {
+ TetheringEventCallback.super.onTetheredInterfacesChanged(interfaces);
+ assertHasAllTetheringInterfaces(interfaces, mTetheredIfaces);
+ mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0));
+ }
+
+ @Override
+ public void onError(String ifName, int error) {
+ mErrorIface = ifName;
+ mErrorCode = error;
+ }
+
+ @Override
+ public void onError(TetheringInterface ifName, int error) {
+ TetheringEventCallback.super.onError(ifName, error);
+ assertEquals(ifName.getInterface(), mErrorIface);
+ assertEquals(error, mErrorCode);
+ mHistory.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error));
+ }
+
+ @Override
+ public void onClientsChanged(Collection<TetheredClient> clients) {
+ mHistory.add(new CallbackValue(CallbackType.ON_CLIENTS, clients, 0));
+ }
+
+ @Override
+ public void onOffloadStatusChanged(int status) {
+ mHistory.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0));
+ }
+
+ private void assertHasAllTetheringInterfaces(Set<TetheringInterface> tetheringIfaces,
+ List<String> ifaces) {
+ // This does not check that the interfaces are the same. This checks that the
+ // List<String> has all the interface names contained by the Set<TetheringInterface>.
+ assertEquals(tetheringIfaces.size(), ifaces.size());
+ for (TetheringInterface tether : tetheringIfaces) {
+ assertTrue("iface " + tether.getInterface()
+ + " seen by new callback but not old callback",
+ ifaces.contains(tether.getInterface()));
+ }
+ }
+
+ public void expectTetherableInterfacesChanged(@NonNull final List<String> regexs,
+ final int type) {
+ assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS,
+ (cv) -> {
+ if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false;
+ final Set<TetheringInterface> interfaces =
+ (Set<TetheringInterface>) cv.callbackParam;
+ return getFirstMatchingTetheringInterface(regexs, type, interfaces) != null;
+ }));
+ }
+
+ public void expectNoTetheringActive() {
+ assertNotNull("At least one tethering type unexpectedly active",
+ mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false;
+
+ return ((Set<TetheringInterface>) cv.callbackParam).isEmpty();
+ }));
+ }
+
+ public TetheringInterface expectTetheredInterfacesChanged(
+ @NonNull final List<String> regexs, final int type) {
+ while (true) {
+ final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true);
+ if (cv == null) {
+ fail("No expected tethered ifaces callback, expected type: " + type);
+ }
+
+ if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) continue;
+
+ final Set<TetheringInterface> interfaces =
+ (Set<TetheringInterface>) cv.callbackParam;
+
+ final TetheringInterface iface =
+ getFirstMatchingTetheringInterface(regexs, type, interfaces);
+
+ if (iface != null) return iface;
+ }
+ }
+
+ public void expectCallbackStarted() {
+ // This method uses its own readhead because it just check whether last tethering status
+ // is updated after TetheringEventCallback get registered but do not check content
+ // of received callbacks. Using shared readhead (mCurrent) only when the callbacks the
+ // method polled is also not necessary for other methods which using shared readhead.
+ // All of methods using mCurrent is order mattered.
+ final ArrayTrackRecord<CallbackValue>.ReadHead history =
+ mHistory.newReadHead();
+ int receivedBitMap = 0;
+ // The each bit represent a type from CallbackType.ON_*.
+ // Expect all of callbacks except for ON_ERROR.
+ final int expectedBitMap = 0xff ^ (1 << CallbackType.ON_ERROR.ordinal());
+ // Receive ON_ERROR on started callback is not matter. It just means tethering is
+ // failed last time, should able to continue the test this time.
+ while ((receivedBitMap & expectedBitMap) != expectedBitMap) {
+ final CallbackValue cv = history.poll(TIMEOUT_MS, c -> true);
+ if (cv == null) {
+ fail("No expected callbacks, " + "expected bitmap: "
+ + expectedBitMap + ", actual: " + receivedBitMap);
+ }
+
+ receivedBitMap |= (1 << cv.callbackType.ordinal());
+ }
+ }
+
+ public void expectOneOfOffloadStatusChanged(int... offloadStatuses) {
+ assertNotNull("No offload status changed", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType != CallbackType.ON_OFFLOAD_STATUS) return false;
+
+ final int status = (int) cv.callbackParam;
+ for (int offloadStatus : offloadStatuses) {
+ if (offloadStatus == status) return true;
+ }
+
+ return false;
+ }));
+ }
+
+ public void expectErrorOrTethered(final TetheringInterface iface) {
+ assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType == CallbackType.ON_ERROR
+ && iface.equals((TetheringInterface) cv.callbackParam)) {
+ return true;
+ }
+ if (cv.callbackType == CallbackType.ON_TETHERED_IFACES
+ && ((Set<TetheringInterface>) cv.callbackParam).contains(iface)) {
+ return true;
+ }
+
+ return false;
+ }));
+ }
+
+ public Network getCurrentValidUpstream() {
+ final CallbackValue result = mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ return (cv.callbackType == CallbackType.ON_UPSTREAM)
+ && cv.callbackParam != null;
+ });
+
+ assertNotNull("No valid upstream", result);
+ return (Network) result.callbackParam;
+ }
+
+ public void assumeTetheringSupported() {
+ final ArrayTrackRecord<CallbackValue>.ReadHead history =
+ mHistory.newReadHead();
+ assertNotNull("No onSupported callback", history.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType != CallbackType.ON_SUPPORTED) return false;
+
+ assumeTrue(cv.callbackParam2 == 1 /* supported */);
+ return true;
+ }));
+ }
+
+ public void assumeWifiTetheringSupported(final Context ctx) throws Exception {
+ assumeTetheringSupported();
+
+ assumeTrue(!getTetheringInterfaceRegexps().getTetherableWifiRegexs().isEmpty());
+
+ final PackageManager pm = ctx.getPackageManager();
+ assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_WIFI));
+
+ WifiManager wm = ctx.getSystemService(WifiManager.class);
+ // Wifi feature flags only work when wifi is on.
+ final boolean previousWifiEnabledState = wm.isWifiEnabled();
+ try {
+ if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi enable");
+ waitForWifiEnabled(ctx);
+ assumeTrue(wm.isPortableHotspotSupported());
+ } finally {
+ if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi disable");
+ }
+ }
+
+ public TetheringInterfaceRegexps getTetheringInterfaceRegexps() {
+ return mTetherableRegex;
+ }
+ }
+
+ private static void waitForWifiEnabled(final Context ctx) throws Exception {
+ WifiManager wm = ctx.getSystemService(WifiManager.class);
+ if (wm.isWifiEnabled()) return;
+
+ final ConditionVariable mWaiting = new ConditionVariable();
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
+ if (wm.isWifiEnabled()) mWaiting.open();
+ }
+ }
+ };
+ try {
+ ctx.registerReceiver(receiver, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
+ if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
+ assertTrue("Wifi did not become enabled after " + DEFAULT_TIMEOUT_MS + "ms",
+ wm.isWifiEnabled());
+ }
+ } finally {
+ ctx.unregisterReceiver(receiver);
+ }
+ }
+
+ public TestTetheringEventCallback registerTetheringEventCallback() {
+ final TestTetheringEventCallback tetherEventCallback =
+ new TestTetheringEventCallback();
+
+ mTm.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback);
+ tetherEventCallback.expectCallbackStarted();
+
+ return tetherEventCallback;
+ }
+
+ public void unregisterTetheringEventCallback(final TestTetheringEventCallback callback) {
+ mTm.unregisterTetheringEventCallback(callback);
+ }
+
+ private static List<String> getWifiTetherableInterfaceRegexps(
+ final TestTetheringEventCallback callback) {
+ return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
+ }
+
+ public static boolean isWifiTetheringSupported(final TestTetheringEventCallback callback) {
+ return !getWifiTetherableInterfaceRegexps(callback).isEmpty();
+ }
+
+ public TetheringInterface startWifiTethering(final TestTetheringEventCallback callback)
+ throws InterruptedException {
+ final List<String> wifiRegexs = getWifiTetherableInterfaceRegexps(callback);
+
+ final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setShouldShowEntitlementUi(false).build();
+ mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.verifyTetheringStarted();
+
+ final TetheringInterface iface =
+ callback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI);
+
+ callback.expectOneOfOffloadStatusChanged(
+ TETHER_HARDWARE_OFFLOAD_STARTED,
+ TETHER_HARDWARE_OFFLOAD_FAILED);
+
+ return iface;
+ }
+
+ private static class StopSoftApCallback implements SoftApCallback {
+ private final ConditionVariable mWaiting = new ConditionVariable();
+ @Override
+ public void onStateChanged(int state, int failureReason) {
+ if (state == WifiManager.WIFI_AP_STATE_DISABLED) mWaiting.open();
+ }
+
+ @Override
+ public void onConnectedClientsChanged(List<WifiClient> clients) { }
+
+ public void waitForSoftApStopped() {
+ if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
+ fail("stopSoftAp Timeout");
+ }
+ }
+ }
+
+ // Wait for softAp to be disabled. This is necessary on devices where stopping softAp
+ // deletes the interface. On these devices, tethering immediately stops when the softAp
+ // interface is removed, but softAp is not yet fully disabled. Wait for softAp to be
+ // fully disabled, because otherwise the next test might fail because it attempts to
+ // start softAp before it's fully stopped.
+ public void expectSoftApDisabled() {
+ final StopSoftApCallback callback = new StopSoftApCallback();
+ try {
+ mWm.registerSoftApCallback(c -> c.run(), callback);
+ // registerSoftApCallback will immediately call the callback with the current state, so
+ // this callback will fire even if softAp is already disabled.
+ callback.waitForSoftApStopped();
+ } finally {
+ mWm.unregisterSoftApCallback(callback);
+ }
+ }
+
+ public void stopWifiTethering(final TestTetheringEventCallback callback) {
+ mTm.stopTethering(TETHERING_WIFI);
+ expectSoftApDisabled();
+ callback.expectNoTetheringActive();
+ callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ }
+}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
new file mode 100644
index 0000000..52ce83a
--- /dev/null
+++ b/tests/cts/tethering/Android.bp
@@ -0,0 +1,98 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "CtsTetheringTestDefaults",
+ defaults: ["cts_defaults"],
+
+ libs: [
+ "android.test.base",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ static_libs: [
+ "TetheringCommonTests",
+ "compatibility-device-util-axt",
+ "cts-net-utils",
+ "net-tests-utils",
+ "ctstestrunner-axt",
+ "junit",
+ "junit-params",
+ ],
+
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+
+ // Change to system current when TetheringManager move to bootclass path.
+ platform_apis: true,
+}
+
+// Tethering CTS tests that target the latest released SDK. These tests can be installed on release
+// devices which has equal or lowner sdk version than target sdk and are useful for qualifying
+// mainline modules on release devices.
+android_test {
+ name: "CtsTetheringTestLatestSdk",
+ defaults: ["CtsTetheringTestDefaults"],
+
+ min_sdk_version: "30",
+ target_sdk_version: "30",
+
+ static_libs: [
+ "TetheringIntegrationTestsLatestSdkLib",
+ ],
+
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+
+ test_config_template: "AndroidTestTemplate.xml",
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+}
+
+// Tethering CTS tests for development and release. These tests always target the platform SDK
+// version, and are subject to all the restrictions appropriate to that version. Before SDK
+// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
+// devices.
+android_test {
+ name: "CtsTetheringTest",
+ defaults: ["CtsTetheringTestDefaults"],
+
+ static_libs: [
+ "TetheringIntegrationTestsLib",
+ ],
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+ test_config_template: "AndroidTestTemplate.xml",
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+}
diff --git a/tests/cts/tethering/AndroidManifest.xml b/tests/cts/tethering/AndroidManifest.xml
new file mode 100644
index 0000000..911dbf2
--- /dev/null
+++ b/tests/cts/tethering/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.tethering.cts">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.tethering.cts"
+ android:label="CTS tests of android.tethering">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener" />
+ </instrumentation>
+
+</manifest>
diff --git a/tests/cts/tethering/AndroidTestTemplate.xml b/tests/cts/tethering/AndroidTestTemplate.xml
new file mode 100644
index 0000000..491b004
--- /dev/null
+++ b/tests/cts/tethering/AndroidTestTemplate.xml
@@ -0,0 +1,35 @@
+<?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="Config for {MODULE}">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="not-shardable" value="true" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="{MODULE}.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.tethering.cts" />
+ </test>
+
+ <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/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
new file mode 100644
index 0000000..0a5e506
--- /dev/null
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -0,0 +1,466 @@
+/*
+ * 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 android.tethering.test;
+
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+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_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
+
+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;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TetheringInterface;
+import android.net.TetheringManager;
+import android.net.TetheringManager.OnTetheringEntitlementResultListener;
+import android.net.TetheringManager.TetheringInterfaceRegexps;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.cts.util.CtsNetUtils;
+import android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class TetheringManagerTest {
+
+ private Context mContext;
+
+ private ConnectivityManager mCm;
+ private TetheringManager mTM;
+ private WifiManager mWm;
+ private PackageManager mPm;
+
+ private TetherChangeReceiver mTetherChangeReceiver;
+ private CtsNetUtils mCtsNetUtils;
+ private CtsTetheringUtils mCtsTetheringUtils;
+
+ private static final int DEFAULT_TIMEOUT_MS = 60_000;
+
+ private void adoptShellPermissionIdentity() {
+ final UiAutomation uiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uiAutomation.adoptShellPermissionIdentity();
+ }
+
+ private void dropShellPermissionIdentity() {
+ final UiAutomation uiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uiAutomation.dropShellPermissionIdentity();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ adoptShellPermissionIdentity();
+ mContext = InstrumentationRegistry.getContext();
+ mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mTM = (TetheringManager) mContext.getSystemService(Context.TETHERING_SERVICE);
+ mWm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ mPm = mContext.getPackageManager();
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ mCtsTetheringUtils = new CtsTetheringUtils(mContext);
+ mTetherChangeReceiver = new TetherChangeReceiver();
+ final IntentFilter filter = new IntentFilter(
+ TetheringManager.ACTION_TETHER_STATE_CHANGED);
+ final Intent intent = mContext.registerReceiver(mTetherChangeReceiver, filter);
+ if (intent != null) mTetherChangeReceiver.onReceive(null, intent);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTM.stopAllTethering();
+ mContext.unregisterReceiver(mTetherChangeReceiver);
+ dropShellPermissionIdentity();
+ }
+
+ private class TetherChangeReceiver extends BroadcastReceiver {
+ private class TetherState {
+ final ArrayList<String> mAvailable;
+ final ArrayList<String> mActive;
+ final ArrayList<String> mErrored;
+
+ TetherState(Intent intent) {
+ mAvailable = intent.getStringArrayListExtra(
+ TetheringManager.EXTRA_AVAILABLE_TETHER);
+ mActive = intent.getStringArrayListExtra(
+ TetheringManager.EXTRA_ACTIVE_TETHER);
+ mErrored = intent.getStringArrayListExtra(
+ TetheringManager.EXTRA_ERRORED_TETHER);
+ }
+ }
+
+ @Override
+ public void onReceive(Context content, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(TetheringManager.ACTION_TETHER_STATE_CHANGED)) {
+ mResult.add(new TetherState(intent));
+ }
+ }
+
+ public final LinkedBlockingQueue<TetherState> mResult = new LinkedBlockingQueue<>();
+
+ // Expects that tethering reaches the desired state.
+ // - If active is true, expects that tethering is enabled on at least one interface
+ // matching ifaceRegexs.
+ // - If active is false, expects that tethering is disabled on all the interfaces matching
+ // ifaceRegexs.
+ // Fails if any interface matching ifaceRegexs becomes errored.
+ public void expectTethering(final boolean active, final String[] ifaceRegexs) {
+ while (true) {
+ final TetherState state = pollAndAssertNoError(DEFAULT_TIMEOUT_MS, ifaceRegexs);
+ assertNotNull("Did not receive expected state change, active: " + active, state);
+
+ if (isIfaceActive(ifaceRegexs, state) == active) return;
+ }
+ }
+
+ private TetherState pollAndAssertNoError(final int timeout, final String[] ifaceRegexs) {
+ final TetherState state = pollTetherState(timeout);
+ assertNoErroredIfaces(state, ifaceRegexs);
+ return state;
+ }
+
+ private TetherState pollTetherState(final int timeout) {
+ try {
+ return mResult.poll(timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("No result after " + timeout + " ms");
+ return null;
+ }
+ }
+
+ private boolean isIfaceActive(final String[] ifaceRegexs, final TetherState state) {
+ return isAnyIfaceMatch(ifaceRegexs, state.mActive);
+ }
+
+ private void assertNoErroredIfaces(final TetherState state, final String[] ifaceRegexs) {
+ if (state == null || state.mErrored == null) return;
+
+ if (isAnyIfaceMatch(ifaceRegexs, state.mErrored)) {
+ fail("Found failed tethering interfaces: " + Arrays.toString(state.mErrored.toArray()));
+ }
+ }
+ }
+
+ @Test
+ public void testStartTetheringWithStateChangeBroadcast() throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+
+ final String[] wifiRegexs = mTM.getTetherableWifiRegexs();
+ final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setShouldShowEntitlementUi(false).build();
+ mTM.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.verifyTetheringStarted();
+
+ mTetherChangeReceiver.expectTethering(true /* active */, wifiRegexs);
+
+ mTM.stopTethering(TETHERING_WIFI);
+ mCtsTetheringUtils.expectSoftApDisabled();
+ mTetherChangeReceiver.expectTethering(false /* active */, wifiRegexs);
+ }
+
+ @Test
+ public void testTetheringRequest() {
+ final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ assertEquals(TETHERING_WIFI, tr.getTetheringType());
+ assertNull(tr.getLocalIpv4Address());
+ assertNull(tr.getClientStaticIpv4Address());
+ assertFalse(tr.isExemptFromEntitlementCheck());
+ assertTrue(tr.getShouldShowEntitlementUi());
+
+ final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
+ final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
+ final TetheringRequest tr2 = new TetheringRequest.Builder(TETHERING_USB)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+
+ assertEquals(localAddr, tr2.getLocalIpv4Address());
+ assertEquals(clientAddr, tr2.getClientStaticIpv4Address());
+ assertEquals(TETHERING_USB, tr2.getTetheringType());
+ assertTrue(tr2.isExemptFromEntitlementCheck());
+ assertFalse(tr2.getShouldShowEntitlementUi());
+ }
+
+ @Test
+ public void testRegisterTetheringEventCallback() throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+ tetherEventCallback.expectNoTetheringActive();
+
+ final TetheringInterface tetheredIface =
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ assertNotNull(tetheredIface);
+ final String wifiTetheringIface = tetheredIface.getInterface();
+
+ mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
+
+ try {
+ final int ret = mTM.tether(wifiTetheringIface);
+ // There is no guarantee that the wifi interface will be available after disabling
+ // the hotspot, so don't fail the test if the call to tether() fails.
+ if (ret == TETHER_ERROR_NO_ERROR) {
+ // If calling #tether successful, there is a callback to tell the result of
+ // tethering setup.
+ tetherEventCallback.expectErrorOrTethered(
+ new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ }
+ } finally {
+ mTM.untether(wifiTetheringIface);
+ }
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+ }
+
+ @Test
+ public void testGetTetherableInterfaceRegexps() {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ tetherEventCallback.assumeTetheringSupported();
+
+ final TetheringInterfaceRegexps tetherableRegexs =
+ tetherEventCallback.getTetheringInterfaceRegexps();
+ final List<String> wifiRegexs = tetherableRegexs.getTetherableWifiRegexs();
+ final List<String> usbRegexs = tetherableRegexs.getTetherableUsbRegexs();
+ final List<String> btRegexs = tetherableRegexs.getTetherableBluetoothRegexs();
+
+ assertEquals(wifiRegexs, Arrays.asList(mTM.getTetherableWifiRegexs()));
+ assertEquals(usbRegexs, Arrays.asList(mTM.getTetherableUsbRegexs()));
+ assertEquals(btRegexs, Arrays.asList(mTM.getTetherableBluetoothRegexs()));
+
+ //Verify that any regex name should only contain in one array.
+ wifiRegexs.forEach(s -> assertFalse(usbRegexs.contains(s)));
+ wifiRegexs.forEach(s -> assertFalse(btRegexs.contains(s)));
+ usbRegexs.forEach(s -> assertFalse(btRegexs.contains(s)));
+
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+
+ @Test
+ public void testStopAllTethering() throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ // TODO: start ethernet tethering here when TetheringManagerTest is moved to
+ // TetheringIntegrationTest.
+
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ mTM.stopAllTethering();
+ tetherEventCallback.expectNoTetheringActive();
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+ }
+
+ @Test
+ public void testEnableTetheringPermission() throws Exception {
+ dropShellPermissionIdentity();
+ final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+ mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+ c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ }
+
+ private class EntitlementResultListener implements OnTetheringEntitlementResultListener {
+ private final CompletableFuture<Integer> future = new CompletableFuture<>();
+
+ @Override
+ public void onTetheringEntitlementResult(int result) {
+ future.complete(result);
+ }
+
+ public int get(long timeout, TimeUnit unit) throws Exception {
+ return future.get(timeout, unit);
+ }
+
+ }
+
+ private void assertEntitlementResult(final Consumer<EntitlementResultListener> functor,
+ final int expect) throws Exception {
+ final EntitlementResultListener listener = new EntitlementResultListener();
+ functor.accept(listener);
+
+ assertEquals(expect, listener.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testRequestLatestEntitlementResult() throws Exception {
+ assumeTrue(mTM.isTetheringSupported());
+ // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+ // result(TETHER_ERROR_ENTITLEMENT_UNKNOWN due to invalid downstream type) via listener.
+ assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI_P2P, false, c -> c.run(), listener),
+ TETHER_ERROR_ENTITLEMENT_UNKNOWN);
+
+ // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+ // result(TETHER_ERROR_ENTITLEMENT_UNKNOWN due to invalid downstream type) via receiver.
+ assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI_P2P,
+ new ResultReceiver(null /* handler */) {
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ listener.onTetheringEntitlementResult(resultCode);
+ }
+ }, false),
+ TETHER_ERROR_ENTITLEMENT_UNKNOWN);
+
+ // Do not request TETHERING_WIFI entitlement result if TETHERING_WIFI is not available.
+ assumeTrue(mTM.getTetherableWifiRegexs().length > 0);
+
+ // Verify that null listener will cause IllegalArgumentException.
+ try {
+ mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI, false, c -> c.run(), null);
+ } catch (IllegalArgumentException expect) { }
+
+ // Override carrier config to ignore entitlement check.
+ final PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
+ overrideCarrierConfig(bundle);
+
+ // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+ // result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
+ assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
+
+ // Reset carrier config.
+ overrideCarrierConfig(null);
+ }
+
+ private void overrideCarrierConfig(PersistableBundle bundle) {
+ final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+ .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ final int subId = SubscriptionManager.getDefaultSubscriptionId();
+ configManager.overrideConfig(subId, bundle);
+ }
+
+ @Test
+ public void testTetheringUpstream() throws Exception {
+ assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+
+ boolean previousWifiEnabledState = false;
+
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+ tetherEventCallback.expectNoTetheringActive();
+
+ previousWifiEnabledState = mWm.isWifiEnabled();
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.ensureWifiDisconnected(null);
+ }
+
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ Network activeNetwork = null;
+ try {
+ mCm.registerDefaultNetworkCallback(networkCallback);
+ activeNetwork = networkCallback.waitForAvailable();
+ } finally {
+ mCm.unregisterNetworkCallback(networkCallback);
+ }
+
+ assertNotNull("No active network. Please ensure the device has working mobile data.",
+ activeNetwork);
+ final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
+
+ // If active nework is ETHERNET, tethering may not use cell network as upstream.
+ assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
+
+ assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
+
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ final boolean dunRequired = telephonyManager.isTetheringApnRequired();
+ final int expectedCap = dunRequired ? NET_CAPABILITY_DUN : NET_CAPABILITY_INTERNET;
+ final Network network = tetherEventCallback.getCurrentValidUpstream();
+ final NetworkCapabilities netCap = mCm.getNetworkCapabilities(network);
+ assertTrue(netCap.hasTransport(TRANSPORT_CELLULAR));
+ assertTrue(netCap.hasCapability(expectedCap));
+
+ mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.connectToWifi();
+ }
+ }
+ }
+}
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index 58ece37..806f805 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -16,11 +16,7 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
java_test_host {
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 39c424e..81f9b48 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -16,11 +16,7 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test {
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index 1535f3d..8011540 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -11,11 +11,7 @@
// dependent libraries.
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test {
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 6f503ac..beae0cf 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -4,10 +4,10 @@
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
+ // all of the 'license_kinds' from "Android-Apache-2.0"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
java_defaults {
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 22a04f5..1c1ba9e 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -1,10 +1,6 @@
package {
// See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
cc_library_shared {